UNPKG

@jupiterone/jupiterone-mcp

Version:

Model Context Protocol server for JupiterOne account rules and rule details

156 lines 7.22 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JupiterOneMcpServer = void 0; const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const zod_1 = require("zod"); const jupiterone_client_js_1 = require("../client/jupiterone-client.js"); const package_json_1 = require("../../package.json"); const j1ql_validator_js_1 = require("../utils/j1ql-validator.js"); const index_js_1 = require("./tools/index.js"); const errors_js_1 = require("./tools/errors.js"); const telemetry_js_1 = require("./tools/telemetry.js"); const icon_js_1 = require("./icon.js"); const usage_js_1 = require("./tools/usage.js"); class JupiterOneMcpServer { server; client; validator; isMultiTenant; telemetry; envAccountId; resolveAccounts; constructor(config, options = {}) { this.client = new jupiterone_client_js_1.JupiterOneClient(config); this.validator = new j1ql_validator_js_1.J1QLValidator(this.client.j1qlService); this.server = new mcp_js_1.McpServer({ name: 'jupiterone-mcp', title: 'JupiterOne', version: package_json_1.version, websiteUrl: 'https://www.jupiterone.com', icons: [icon_js_1.JUPITERONE_ICON], }); this.isMultiTenant = !config.accountId; this.envAccountId = config.accountId || undefined; this.telemetry = options.telemetry ?? telemetry_js_1.stderrTelemetry; this.resolveAccounts = options.resolveAccounts; for (const tool of index_js_1.allTools) { // Account-discovery tools need a host-supplied resolver (the multi-tenant remote); skip them // when none is provided (stdio / fixed-account connectors have nothing to list or select). if (tool.requiresAccountResolver && !this.resolveAccounts) continue; this.register(tool); } } /** * Build the per-call tool context passed to handlers — currently just the host-supplied account * resolver, consumed by the account-discovery tool. */ toolContext() { return { resolveAccounts: this.resolveAccounts }; } /** * Single registration chokepoint: injects the per-request `accountId` parameter when no env * account is configured, then wraps every handler with account-scoped client cloning, sanitized * error formatting (C2), and per-call telemetry (O1). */ register(tool) { const needsAccount = this.isMultiTenant && !tool.accountAgnostic; const inputSchema = { ...tool.inputSchema, reason: zod_1.z .string() .optional() .describe('Optional: a brief statement of why you are calling this tool — what you are trying to find out or accomplish. Recorded for usage analytics only; does not affect behavior.'), ...(needsAccount ? { accountId: zod_1.z .string() .describe('JupiterOne Account ID to act on. If the user has not specified one, call `list-accounts` to discover the accounts they can access, then pass the returned accountId — not the display name. To act across multiple accounts, repeat the call once per accountId.'), } : {}), }; const registerTool = this.server.registerTool.bind(this.server); registerTool(tool.name, { title: tool.title, description: tool.description, inputSchema, ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}), // The connector directory validates `annotations.title`, so mirror the per-tool title into // annotations (the SDK's top-level `title` above is a separate field it does not check). annotations: { ...tool.annotations, title: tool.title }, }, (params) => this.invoke(tool, params)); } async invoke(tool, params) { // `reason` (self-reported agent intent) is injected on every tool; strip it before the handler. const { reason: rawReason, ...rest } = params; const reason = typeof rawReason === 'string' && rawReason.length > 0 ? (0, usage_js_1.truncateReason)(rawReason) : undefined; let client = this.client; let validator = this.validator; let toolParams = rest; let accountId = this.envAccountId; if (this.isMultiTenant && !tool.accountAgnostic) { const { accountId: requested, ...otherParams } = rest; if (!requested || typeof requested !== 'string') { return { content: [ { type: 'text', text: 'Error: accountId is required for this operation. Call `list-accounts` to discover the accounts you can access, then retry with the accountId parameter set.', }, ], isError: true, }; } accountId = requested; client = this.client.cloneWithAccountId(requested); validator = new j1ql_validator_js_1.J1QLValidator(client.j1qlService); toolParams = otherParams; } const startedAt = Date.now(); let outcome = 'success'; let errorClass; let result; try { result = await tool.handler(toolParams, client, validator, this.toolContext()); if (result.isError) outcome = 'error'; return result; } catch (error) { outcome = 'error'; errorClass = error instanceof Error ? error.constructor.name : 'UnknownError'; // Frame auth/permission failures once, here, so the message can reflect whether account // discovery is available (multi-tenant) — ahead of any tool-specific formatError. const deniedStatus = (0, errors_js_1.accessDeniedStatus)(error); result = deniedStatus !== null ? (0, errors_js_1.accessDeniedResult)(deniedStatus, !!this.resolveAccounts) : tool.formatError ? tool.formatError(error, toolParams, validator) : (0, errors_js_1.toErrorResponse)(error, tool.errorContext); return result; } finally { this.telemetry({ tool: tool.name, outcome, durationMs: Date.now() - startedAt, errorClass, accountId, reason, input: (0, usage_js_1.summarizeInput)(toolParams), result: result ? (0, usage_js_1.summarizeResult)(result) : undefined, }); } } async start() { const transport = new stdio_js_1.StdioServerTransport(); await this.server.connect(transport); } async stop() { // Cleanup if needed } } exports.JupiterOneMcpServer = JupiterOneMcpServer; //# sourceMappingURL=mcp-server.js.map