@jupiterone/jupiterone-mcp
Version:
Model Context Protocol server for JupiterOne account rules and rule details
156 lines • 7.22 kB
JavaScript
;
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