@objectstack/connector-mcp 15.0.0 → 15.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +138 -0
- package/dist/index.d.mts +93 -15
- package/dist/index.d.ts +93 -15
- package/dist/index.js +125 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +122 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/connector-mcp-plugin.test.ts +31 -0
- package/src/connector-mcp-plugin.ts +69 -24
- package/src/index.ts +6 -0
- package/src/mcp-provider.test.ts +193 -0
- package/src/mcp-provider.ts +205 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/mcp-connector.ts","../src/connector-mcp-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/connector-mcp\n *\n * A generic adapter that turns *any* Model Context Protocol (MCP) server into a\n * {@link Connector} registered on the automation engine (ADR-0024). On connect\n * it lists the server's tools and maps each one to a connector action; the\n * baseline `connector_action` node then dispatches calls to the server's\n * `tools/call`. One adapter unlocks the entire MCP ecosystem with no per-server\n * code — and, because MCP is itself an LLM tool protocol, every imported tool\n * doubles as an AI tool under ADR-0011.\n *\n * Open-source scope: the MCP client adapter (stdio + http transports),\n * `tools/list` → actions, `tools/call` dispatch, and operator-supplied static\n * credentials passed through the transport. A curated server registry, managed\n * secrets, per-tenant lifecycle, and sandboxed stdio execution are the\n * enterprise tier (ADR-0024 §4).\n */\n\nexport {\n createMcpConnector,\n type McpConnectorOptions,\n type McpConnectorBundle,\n type McpTransport,\n type McpToolDescriptor,\n type McpClientLike,\n} from './mcp-connector.js';\nexport {\n ConnectorMcpPlugin,\n type ConnectorMcpPluginOptions,\n type ConnectorRegistrySurface,\n} from './connector-mcp-plugin.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * MCP connector — a *generic* adapter that turns any Model Context Protocol\n * server into a {@link Connector} (ADR-0024). Where `connector-rest` and\n * `connector-slack` are concrete, per-service connectors, this one is a single\n * adapter that adopts the entire MCP ecosystem with **no per-server code**:\n *\n * 1. connect to the MCP server over the configured transport,\n * 2. call `tools/list` and map each tool to a connector action\n * (`name → key`, `description → label/description`, `inputSchema → inputSchema`),\n * 3. build an ordinary `type: 'api'` {@link Connector} once, and\n * 4. dispatch each `connector_action` call to the server's `tools/call`.\n *\n * After construction the registry, the `connector_action` node, the discovery\n * route, and the Studio palette all see a plain connector — they never know it\n * is backed by MCP (ADR-0024 §2).\n *\n * **Credentials live with the MCP server, not in `ConnectorSchema`** (ADR-0024\n * §3). The operator supplies `env` (stdio) / `headers` (http) which we pass\n * straight to the transport; they are never copied into the serialized `def`\n * (which is exposed via discovery) and must never be logged.\n *\n * **Trust:** launching a stdio server runs a local process. Sandboxed,\n * multi-tenant execution and managed secrets are the enterprise tier (ADR-0024\n * §4); the open adapter runs an operator-provided server with operator-provided\n * credentials and documents that trust assumption.\n */\n\n/** How to reach the MCP server. */\nexport type McpTransport =\n | {\n kind: 'stdio';\n /** Executable to launch (e.g. `npx`). */\n command: string;\n /** Arguments passed to the command. */\n args?: string[];\n /** Environment variables for the child process — carries credentials. */\n env?: Record<string, string>;\n }\n | {\n kind: 'http';\n /** Streamable-HTTP endpoint of the MCP server. */\n url: string;\n /** Headers sent on every request — carries credentials (e.g. a bearer token). */\n headers?: Record<string, string>;\n };\n\n/** A tool as advertised by an MCP server's `tools/list`. */\nexport interface McpToolDescriptor {\n name: string;\n description?: string;\n /** JSON Schema for the tool's arguments. */\n inputSchema?: Record<string, unknown>;\n /** JSON Schema for the tool's result (optional — many servers omit it). */\n outputSchema?: Record<string, unknown>;\n}\n\n/**\n * The minimal slice of an MCP client the adapter needs. Kept structural so\n * tests can inject a fake and the real SDK stays an implementation detail\n * (mirrors `fetchImpl` injection in `connector-rest`).\n */\nexport interface McpClientLike {\n /** List the server's tools (`tools/list`). */\n listTools(): Promise<McpToolDescriptor[]>;\n /** Invoke a tool (`tools/call`); returns the raw MCP result. */\n callTool(name: string, args: Record<string, unknown>): Promise<unknown>;\n /** Close the connection / tear down the transport. */\n close(): Promise<void>;\n}\n\nexport interface McpConnectorOptions {\n /** Connector machine name (snake_case). Defaults to a slug of `label`, else `mcp`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Connector description for the palette. */\n description?: string;\n /** Icon identifier. Defaults to `plug`. */\n icon?: string;\n /** How to reach the MCP server. */\n transport: McpTransport;\n /** Only expose tools whose name matches (allowlist) — keeps the palette lean. */\n include?: (toolName: string) => boolean;\n /** Identifies this client to the MCP server during the handshake. */\n clientInfo?: { name: string; version: string };\n /**\n * Injected for tests; defaults to the real SDK-backed client. Receives the\n * configured transport and returns a connected {@link McpClientLike}.\n */\n clientFactory?: (transport: McpTransport, clientInfo: { name: string; version: string }) => Promise<McpClientLike>;\n}\n\n/**\n * A connector definition + handlers, ready for `engine.registerConnector()`,\n * plus a `close()` for the connection lifecycle (called by the plugin's stop()).\n */\nexport interface McpConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n /** Tear down the MCP client/connection. */\n close(): Promise<void>;\n}\n\nconst DEFAULT_CLIENT_INFO = { name: 'objectstack-connector-mcp', version: '1.0.0' } as const;\n\n/** Slugify a label into a valid connector `name` (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slugify(input: string): string {\n const slug = input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '');\n if (!slug) return 'mcp';\n // The name must start with a letter or underscore.\n return /^[a-z_]/.test(slug) ? slug : `mcp_${slug}`;\n}\n\n/** Title-case a snake_case name for a default label (`github_issues` → `Github Issues`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n\n/**\n * Normalise an MCP `tools/call` result into the connector handler's return\n * shape, mirroring the `{ ok, … }` envelope the other connectors expose. An MCP\n * result carries `content` blocks and an optional `isError` flag /\n * `structuredContent`; we surface `ok` from `isError` (never throwing on a\n * logical tool error so the flow author can branch on `${node.ok}`).\n */\nfunction normalizeResult(raw: unknown): Record<string, unknown> {\n const result = (raw ?? {}) as Record<string, unknown>;\n const isError = result.isError === true;\n const out: Record<string, unknown> = {\n ok: !isError,\n content: result.content ?? [],\n };\n if (result.structuredContent !== undefined) out.structuredContent = result.structuredContent;\n if (isError) out.isError = true;\n return out;\n}\n\n/**\n * Default per-request timeout (ms) for MCP calls (P1-1). Without it, a hung or\n * unresponsive MCP server stalls the agent turn indefinitely. The SDK aborts the\n * request once this elapses.\n */\nconst MCP_REQUEST_TIMEOUT_MS = 30_000;\n\n/**\n * The default {@link McpClientLike} — lazily imports the official MCP SDK so it\n * is only loaded when a real connection is made (tests inject their own client).\n */\nasync function defaultClientFactory(\n transport: McpTransport,\n clientInfo: { name: string; version: string },\n): Promise<McpClientLike> {\n const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');\n const client = new Client(clientInfo, { capabilities: {} });\n\n if (transport.kind === 'stdio') {\n const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');\n await client.connect(\n new StdioClientTransport({\n command: transport.command,\n args: transport.args,\n env: transport.env,\n }),\n );\n } else {\n const { StreamableHTTPClientTransport } = await import(\n '@modelcontextprotocol/sdk/client/streamableHttp.js'\n );\n await client.connect(\n new StreamableHTTPClientTransport(new URL(transport.url), {\n requestInit: transport.headers ? { headers: transport.headers } : undefined,\n }),\n );\n }\n\n return {\n async listTools() {\n const res = await client.listTools(undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });\n return (res.tools ?? []) as McpToolDescriptor[];\n },\n async callTool(name, args) {\n return client.callTool({ name, arguments: args }, undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });\n },\n async close() {\n await client.close();\n },\n };\n}\n\n/**\n * Connect to an MCP server, discover its tools, and build a {@link Connector}\n * whose actions dispatch to the server's `tools/call`. The connection is held\n * open for the lifetime of the bundle; call {@link McpConnectorBundle.close} to\n * tear it down.\n */\nexport async function createMcpConnector(opts: McpConnectorOptions): Promise<McpConnectorBundle> {\n const clientInfo = opts.clientInfo ?? DEFAULT_CLIENT_INFO;\n const factory = opts.clientFactory ?? defaultClientFactory;\n\n const client = await factory(opts.transport, clientInfo);\n\n let tools: McpToolDescriptor[];\n try {\n tools = await client.listTools();\n } catch (err) {\n // Discovery failed after connecting — release the connection rather than\n // leaking it, then surface the error to the caller (the plugin fail-soft).\n await client.close().catch(() => {});\n throw err;\n }\n\n const include = opts.include ?? (() => true);\n const selected = tools.filter((t) => include(t.name));\n\n const name = opts.name ?? slugify(opts.label ?? 'mcp');\n const label = opts.label ?? titleize(name);\n\n const handlers: McpConnectorBundle['handlers'] = {};\n const def: Connector = {\n name,\n label,\n type: 'api',\n description:\n opts.description ?? `MCP connector exposing ${selected.length} tool(s) from a Model Context Protocol server.`,\n icon: opts.icon ?? 'plug',\n // MCP servers own their own auth (passed via transport env/headers); we\n // do not model the upstream's credentials in ConnectorSchema (ADR-0024 §3).\n authentication: { type: 'none' },\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: selected.map((tool) => ({\n key: tool.name,\n // MCP tool names are machine names; derive a readable label and keep\n // the server's description verbatim (ADR-0024 `description → label/description`).\n label: titleize(slugify(tool.name)),\n description: tool.description,\n // The MCP inputSchema is already JSON Schema — pass it straight through.\n inputSchema: tool.inputSchema,\n // Many servers omit outputSchema; leave it unset when absent (as the\n // REST connector does for untyped responses).\n outputSchema: tool.outputSchema,\n })),\n };\n\n for (const tool of selected) {\n handlers[tool.name] = async (input) => normalizeResult(await client.callTool(tool.name, input));\n }\n\n return {\n def,\n handlers,\n close: () => client.close(),\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createMcpConnector, type McpConnectorOptions } from './mcp-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorMcpPluginOptions extends McpConnectorOptions {}\n\n/**\n * ConnectorMcpPlugin — connects to an MCP server, discovers its tools, and\n * registers them as a single connector on the automation engine (ADR-0024).\n * One generic adapter, configured per server (transport + `include`), never\n * per-server code.\n *\n * Lifecycle: on `start()` it connects and builds the connector once; on\n * `stop()` it tears the MCP connection down. If no automation engine is present\n * — or the server is unreachable at boot — the plugin logs and skips: a missing\n * optional connector is not a fatal error (same posture as `ConnectorRestPlugin`).\n */\nexport class ConnectorMcpPlugin implements Plugin {\n name = 'com.objectstack.connector.mcp';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorMcpPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n private close?: () => Promise<void>;\n\n constructor(options: ConnectorMcpPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available and the MCP server has been queried.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorMcpPlugin: no automation engine — MCP connector not registered');\n return;\n }\n\n let bundle;\n try {\n bundle = await createMcpConnector(this.options);\n } catch (err) {\n // The MCP server is unreachable / failed discovery at boot. Skip the\n // optional connector rather than failing the whole bootstrap.\n ctx.logger.warn(\n `ConnectorMcpPlugin: could not connect to MCP server — connector not registered: ${(err as Error).message}`,\n );\n return;\n }\n\n automation.registerConnector(bundle.def, bundle.handlers);\n this.automation = automation;\n this.connectorName = bundle.def.name;\n this.close = bundle.close;\n ctx.logger.info(\n `ConnectorMcpPlugin: MCP connector '${bundle.def.name}' registered with ${bundle.def.actions?.length ?? 0} action(s)`,\n );\n }\n\n /**\n * Destroy phase — the kernel's shutdown hook (the `Plugin` lifecycle exposes\n * `destroy()`, not `stop()`). Unregister the connector and tear the MCP\n * connection down so no child process / socket is leaked.\n */\n async destroy(): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n if (this.close) {\n try { await this.close(); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/mcp-connector.ts","../src/mcp-provider.ts","../src/connector-mcp-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/connector-mcp\n *\n * A generic adapter that turns *any* Model Context Protocol (MCP) server into a\n * {@link Connector} registered on the automation engine (ADR-0024). On connect\n * it lists the server's tools and maps each one to a connector action; the\n * baseline `connector_action` node then dispatches calls to the server's\n * `tools/call`. One adapter unlocks the entire MCP ecosystem with no per-server\n * code — and, because MCP is itself an LLM tool protocol, every imported tool\n * doubles as an AI tool under ADR-0011.\n *\n * Open-source scope: the MCP client adapter (stdio + http transports),\n * `tools/list` → actions, `tools/call` dispatch, and operator-supplied static\n * credentials passed through the transport. A curated server registry, managed\n * secrets, per-tenant lifecycle, and sandboxed stdio execution are the\n * enterprise tier (ADR-0024 §4).\n */\n\nexport {\n createMcpConnector,\n type McpConnectorOptions,\n type McpConnectorBundle,\n type McpTransport,\n type McpToolDescriptor,\n type McpClientLike,\n} from './mcp-connector.js';\nexport {\n ConnectorMcpPlugin,\n type ConnectorMcpPluginOptions,\n type ConnectorRegistrySurface,\n} from './connector-mcp-plugin.js';\nexport {\n createMcpProviderFactory,\n MCP_PROVIDER_KEY,\n type McpProviderDeps,\n type McpDeclarativeStdioPolicy,\n} from './mcp-provider.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * MCP connector — a *generic* adapter that turns any Model Context Protocol\n * server into a {@link Connector} (ADR-0024). Where `connector-rest` and\n * `connector-slack` are concrete, per-service connectors, this one is a single\n * adapter that adopts the entire MCP ecosystem with **no per-server code**:\n *\n * 1. connect to the MCP server over the configured transport,\n * 2. call `tools/list` and map each tool to a connector action\n * (`name → key`, `description → label/description`, `inputSchema → inputSchema`),\n * 3. build an ordinary `type: 'api'` {@link Connector} once, and\n * 4. dispatch each `connector_action` call to the server's `tools/call`.\n *\n * After construction the registry, the `connector_action` node, the discovery\n * route, and the Studio palette all see a plain connector — they never know it\n * is backed by MCP (ADR-0024 §2).\n *\n * **Credentials live with the MCP server, not in `ConnectorSchema`** (ADR-0024\n * §3). The operator supplies `env` (stdio) / `headers` (http) which we pass\n * straight to the transport; they are never copied into the serialized `def`\n * (which is exposed via discovery) and must never be logged.\n *\n * **Trust:** launching a stdio server runs a local process. Sandboxed,\n * multi-tenant execution and managed secrets are the enterprise tier (ADR-0024\n * §4); the open adapter runs an operator-provided server with operator-provided\n * credentials and documents that trust assumption.\n */\n\n/** How to reach the MCP server. */\nexport type McpTransport =\n | {\n kind: 'stdio';\n /** Executable to launch (e.g. `npx`). */\n command: string;\n /** Arguments passed to the command. */\n args?: string[];\n /** Environment variables for the child process — carries credentials. */\n env?: Record<string, string>;\n }\n | {\n kind: 'http';\n /** Streamable-HTTP endpoint of the MCP server. */\n url: string;\n /** Headers sent on every request — carries credentials (e.g. a bearer token). */\n headers?: Record<string, string>;\n };\n\n/** A tool as advertised by an MCP server's `tools/list`. */\nexport interface McpToolDescriptor {\n name: string;\n description?: string;\n /** JSON Schema for the tool's arguments. */\n inputSchema?: Record<string, unknown>;\n /** JSON Schema for the tool's result (optional — many servers omit it). */\n outputSchema?: Record<string, unknown>;\n}\n\n/**\n * The minimal slice of an MCP client the adapter needs. Kept structural so\n * tests can inject a fake and the real SDK stays an implementation detail\n * (mirrors `fetchImpl` injection in `connector-rest`).\n */\nexport interface McpClientLike {\n /** List the server's tools (`tools/list`). */\n listTools(): Promise<McpToolDescriptor[]>;\n /** Invoke a tool (`tools/call`); returns the raw MCP result. */\n callTool(name: string, args: Record<string, unknown>): Promise<unknown>;\n /** Close the connection / tear down the transport. */\n close(): Promise<void>;\n}\n\nexport interface McpConnectorOptions {\n /** Connector machine name (snake_case). Defaults to a slug of `label`, else `mcp`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Connector description for the palette. */\n description?: string;\n /** Icon identifier. Defaults to `plug`. */\n icon?: string;\n /** How to reach the MCP server. */\n transport: McpTransport;\n /** Only expose tools whose name matches (allowlist) — keeps the palette lean. */\n include?: (toolName: string) => boolean;\n /** Identifies this client to the MCP server during the handshake. */\n clientInfo?: { name: string; version: string };\n /**\n * Injected for tests; defaults to the real SDK-backed client. Receives the\n * configured transport and returns a connected {@link McpClientLike}.\n */\n clientFactory?: (transport: McpTransport, clientInfo: { name: string; version: string }) => Promise<McpClientLike>;\n}\n\n/**\n * A connector definition + handlers, ready for `engine.registerConnector()`,\n * plus a `close()` for the connection lifecycle (called by the plugin's stop()).\n */\nexport interface McpConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n /** Tear down the MCP client/connection. */\n close(): Promise<void>;\n}\n\nconst DEFAULT_CLIENT_INFO = { name: 'objectstack-connector-mcp', version: '1.0.0' } as const;\n\n/** Slugify a label into a valid connector `name` (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slugify(input: string): string {\n const slug = input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '');\n if (!slug) return 'mcp';\n // The name must start with a letter or underscore.\n return /^[a-z_]/.test(slug) ? slug : `mcp_${slug}`;\n}\n\n/** Title-case a snake_case name for a default label (`github_issues` → `Github Issues`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n\n/**\n * Normalise an MCP `tools/call` result into the connector handler's return\n * shape, mirroring the `{ ok, … }` envelope the other connectors expose. An MCP\n * result carries `content` blocks and an optional `isError` flag /\n * `structuredContent`; we surface `ok` from `isError` (never throwing on a\n * logical tool error so the flow author can branch on `${node.ok}`).\n */\nfunction normalizeResult(raw: unknown): Record<string, unknown> {\n const result = (raw ?? {}) as Record<string, unknown>;\n const isError = result.isError === true;\n const out: Record<string, unknown> = {\n ok: !isError,\n content: result.content ?? [],\n };\n if (result.structuredContent !== undefined) out.structuredContent = result.structuredContent;\n if (isError) out.isError = true;\n return out;\n}\n\n/**\n * Default per-request timeout (ms) for MCP calls (P1-1). Without it, a hung or\n * unresponsive MCP server stalls the agent turn indefinitely. The SDK aborts the\n * request once this elapses.\n */\nconst MCP_REQUEST_TIMEOUT_MS = 30_000;\n\n/**\n * The default {@link McpClientLike} — lazily imports the official MCP SDK so it\n * is only loaded when a real connection is made (tests inject their own client).\n */\nasync function defaultClientFactory(\n transport: McpTransport,\n clientInfo: { name: string; version: string },\n): Promise<McpClientLike> {\n const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');\n const client = new Client(clientInfo, { capabilities: {} });\n\n if (transport.kind === 'stdio') {\n const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');\n await client.connect(\n new StdioClientTransport({\n command: transport.command,\n args: transport.args,\n env: transport.env,\n }),\n );\n } else {\n const { StreamableHTTPClientTransport } = await import(\n '@modelcontextprotocol/sdk/client/streamableHttp.js'\n );\n await client.connect(\n new StreamableHTTPClientTransport(new URL(transport.url), {\n requestInit: transport.headers ? { headers: transport.headers } : undefined,\n }),\n );\n }\n\n return {\n async listTools() {\n const res = await client.listTools(undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });\n return (res.tools ?? []) as McpToolDescriptor[];\n },\n async callTool(name, args) {\n return client.callTool({ name, arguments: args }, undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });\n },\n async close() {\n await client.close();\n },\n };\n}\n\n/**\n * Connect to an MCP server, discover its tools, and build a {@link Connector}\n * whose actions dispatch to the server's `tools/call`. The connection is held\n * open for the lifetime of the bundle; call {@link McpConnectorBundle.close} to\n * tear it down.\n */\nexport async function createMcpConnector(opts: McpConnectorOptions): Promise<McpConnectorBundle> {\n const clientInfo = opts.clientInfo ?? DEFAULT_CLIENT_INFO;\n const factory = opts.clientFactory ?? defaultClientFactory;\n\n const client = await factory(opts.transport, clientInfo);\n\n let tools: McpToolDescriptor[];\n try {\n tools = await client.listTools();\n } catch (err) {\n // Discovery failed after connecting — release the connection rather than\n // leaking it, then surface the error to the caller (the plugin fail-soft).\n await client.close().catch(() => {});\n throw err;\n }\n\n const include = opts.include ?? (() => true);\n const selected = tools.filter((t) => include(t.name));\n\n const name = opts.name ?? slugify(opts.label ?? 'mcp');\n const label = opts.label ?? titleize(name);\n\n const handlers: McpConnectorBundle['handlers'] = {};\n const def: Connector = {\n name,\n label,\n type: 'api',\n description:\n opts.description ?? `MCP connector exposing ${selected.length} tool(s) from a Model Context Protocol server.`,\n icon: opts.icon ?? 'plug',\n // MCP servers own their own auth (passed via transport env/headers); we\n // do not model the upstream's credentials in ConnectorSchema (ADR-0024 §3).\n authentication: { type: 'none' },\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: selected.map((tool) => ({\n key: tool.name,\n // MCP tool names are machine names; derive a readable label and keep\n // the server's description verbatim (ADR-0024 `description → label/description`).\n label: titleize(slugify(tool.name)),\n description: tool.description,\n // The MCP inputSchema is already JSON Schema — pass it straight through.\n inputSchema: tool.inputSchema,\n // Many servers omit outputSchema; leave it unset when absent (as the\n // REST connector does for untyped responses).\n outputSchema: tool.outputSchema,\n })),\n };\n\n for (const tool of selected) {\n handlers[tool.name] = async (input) => normalizeResult(await client.callTool(tool.name, input));\n }\n\n return {\n def,\n handlers,\n close: () => client.close(),\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectstack/spec/integration';\nimport { ConnectorUpstreamUnavailableError } from '@objectstack/spec/integration';\nimport { createMcpConnector, type McpConnectorOptions, type McpTransport } from './mcp-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'mcp'` is materialized by this factory.\n */\nexport const MCP_PROVIDER_KEY = 'mcp';\n\n/**\n * Host policy for **declarative** stdio transports (#3055). A stdio transport\n * launches a local child process, and declarative entries arrive through\n * metadata — including a runtime Studio publish — so spawning from them is\n * gated OFF by default:\n *\n * - `undefined` / `false` — deny (default): a `provider: 'mcp'` entry with a\n * stdio transport is rejected as a configuration fault.\n * - `string[]` — allowlist: the transport's `command` must strictly equal one\n * of the listed commands. NOTE this is a coarse trust boundary — listing a\n * launcher like `npx` effectively allows any package it can run; list the\n * specific server binaries you trust. Sandboxed execution is the enterprise\n * tier (ADR-0024 §4).\n * - `true` — allow any command (explicit full trust; hosts that treat every\n * metadata author as an operator).\n *\n * Hand-wired connectors (plugin instance options / `createMcpConnector`) are\n * NOT subject to this policy: their command was written in host code, a\n * different trust anchor than metadata.\n */\nexport type McpDeclarativeStdioPolicy = boolean | string[];\n\n/** Injectable dependencies for {@link createMcpProviderFactory} (tests). */\nexport interface McpProviderDeps {\n /** Injected MCP client factory; defaults to the SDK-backed client. */\n clientFactory?: McpConnectorOptions['clientFactory'];\n /** Policy for declarative stdio transports (#3055). Default: deny. */\n declarativeStdio?: McpDeclarativeStdioPolicy;\n}\n\n/** Shape of `providerConfig` for a `provider: 'mcp'` declarative instance. */\ninterface McpProviderConfig {\n /** How to reach the MCP server (stdio or streamable-http). */\n transport?: unknown;\n /** Optional tool-name allowlist — only these tools become actions. */\n include?: unknown;\n}\n\nfunction isStringRecord(v: unknown): v is Record<string, string> {\n if (!v || typeof v !== 'object' || Array.isArray(v)) return false;\n return Object.values(v as Record<string, unknown>).every((x) => typeof x === 'string');\n}\n\n/**\n * Fold the resolved instance `auth` into an MCP **http** transport's headers\n * (ADR-0024 keeps MCP credentials with the transport). `credentialRef` has\n * already been resolved upstream, so this only maps the static credential to the\n * right header. Not applied to stdio transports — a stdio server receives its\n * credentials through `transport.env`.\n */\nfunction applyAuthToHeaders(\n auth: ResolvedConnectorAuth | undefined,\n headers: Record<string, string>,\n): void {\n if (!auth || auth.type === 'none') return;\n switch (auth.type) {\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic':\n headers['Authorization'] = `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`;\n return;\n case 'api-key':\n // Header-based only for MCP http (query-param keys are not part of the transport).\n if (!auth.paramName) headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\n/** Validate + normalize `providerConfig.transport`, injecting resolved auth for http. */\nfunction normalizeTransport(\n raw: unknown,\n connectorName: string,\n auth: ResolvedConnectorAuth | undefined,\n): McpTransport {\n if (!raw || typeof raw !== 'object') {\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' requires providerConfig.transport ` +\n `({ kind: 'stdio', command, ... } or { kind: 'http', url, ... }).`,\n );\n }\n const t = raw as Record<string, unknown>;\n if (t.kind === 'stdio') {\n if (typeof t.command !== 'string' || t.command.length === 0) {\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' stdio transport requires a 'command' string.`,\n );\n }\n return {\n kind: 'stdio',\n command: t.command,\n args: Array.isArray(t.args) ? t.args.map((a) => String(a)) : undefined,\n env: isStringRecord(t.env) ? t.env : undefined,\n };\n }\n if (t.kind === 'http') {\n if (typeof t.url !== 'string' || t.url.length === 0) {\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' http transport requires a 'url' string.`,\n );\n }\n const headers: Record<string, string> = { ...(isStringRecord(t.headers) ? t.headers : {}) };\n applyAuthToHeaders(auth, headers);\n return { kind: 'http', url: t.url, headers: Object.keys(headers).length > 0 ? headers : undefined };\n }\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' providerConfig.transport.kind must be 'stdio' or 'http'.`,\n );\n}\n\n/**\n * Enforce the {@link McpDeclarativeStdioPolicy} for one declarative instance\n * (#3055). Throws a **plain** Error on violation: a security-policy rejection\n * is a configuration fault — fatal at boot, skipped+logged on reload — and must\n * never be classified upstream-unavailable (it cannot be retried into\n * existence).\n */\nfunction assertDeclarativeStdioAllowed(\n policy: McpDeclarativeStdioPolicy | undefined,\n command: string,\n connectorName: string,\n): void {\n if (policy === true) return;\n if (Array.isArray(policy)) {\n if (policy.includes(command)) return;\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' declares a stdio transport with command '${command}', ` +\n `which is not in the host's declarativeStdio allowlist [${policy.join(', ')}]. ` +\n `Add the command to new ConnectorMcpPlugin({ declarativeStdio: [...] }) if this server is trusted (#3055).`,\n );\n }\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' declares a stdio transport (command '${command}'), ` +\n `but declarative stdio transports are disabled by default — a stdio transport launches a local process ` +\n `from stack metadata (including runtime Studio publishes). If this server is trusted, opt in deliberately: ` +\n `new ConnectorMcpPlugin({ declarativeStdio: ['${command}'] }) — or use an http transport (#3055, ADR-0024 §4).`,\n );\n}\n\n/**\n * Build the `mcp` {@link ConnectorProviderFactory} (ADR-0097 / ADR-0024). At boot\n * the automation service invokes it for each `provider: 'mcp'` declarative\n * instance: it connects to the MCP server named by `providerConfig.transport`,\n * lists its tools, and produces the same `{ def, handlers, close }` bundle\n * {@link createMcpConnector} builds for a hand-wired MCP connector — one action\n * per tool, dispatched to the server's `tools/call`.\n *\n * Stdio transports on declarative instances are policy-gated (default deny) —\n * see {@link McpDeclarativeStdioPolicy} (#3055).\n *\n * The connection is opened at materialization. Faults are classified (#3017):\n * an invalid transport shape is a *configuration* fault and throws plain —\n * fatal at boot per the ADR-0097 fail-loud contract — while a connect /\n * `tools/list` failure (server down, refused, timed out) is an *operational*\n * fault and throws {@link ConnectorUpstreamUnavailableError}, which the\n * materializer turns into a degraded instance that is retried with backoff\n * instead of aborting the whole app boot.\n */\nexport function createMcpProviderFactory(deps: McpProviderDeps = {}): ConnectorProviderFactory {\n return async (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as McpProviderConfig;\n const transport = normalizeTransport(cfg.transport, ctx.name, ctx.auth);\n if (transport.kind === 'stdio') {\n assertDeclarativeStdioAllowed(deps.declarativeStdio, transport.command, ctx.name);\n }\n const includeList = Array.isArray(cfg.include)\n ? cfg.include.filter((x): x is string => typeof x === 'string')\n : undefined;\n const include = includeList ? (toolName: string) => includeList.includes(toolName) : undefined;\n\n let bundle;\n try {\n bundle = await createMcpConnector({\n name: ctx.name,\n label: ctx.label,\n description: ctx.description,\n transport,\n include,\n clientFactory: deps.clientFactory,\n });\n } catch (err) {\n // Everything past transport validation is talking to the server (connect,\n // handshake, tools/list) — operational, hence retryable. A credential the\n // server rejects also lands here: indistinguishable from the outside, and\n // retrying it is loud (logged per attempt), never silent.\n throw new ConnectorUpstreamUnavailableError(\n `connector-mcp provider: connector '${ctx.name}' could not reach its MCP server: ${(err as Error).message}`,\n { cause: err },\n );\n }\n return { def: bundle.def, handlers: bundle.handlers, close: bundle.close };\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createMcpConnector, type McpConnectorOptions } from './mcp-connector.js';\nimport {\n createMcpProviderFactory,\n MCP_PROVIDER_KEY,\n type McpDeclarativeStdioPolicy,\n} from './mcp-provider.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so the plugin needs no runtime dependency on\n * `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Options for {@link ConnectorMcpPlugin}. All optional (ADR-0097): with no\n * `transport` the plugin contributes only the `mcp` provider factory — so a\n * stack can declare `provider: 'mcp'` instances as pure metadata. Supply a\n * `transport` to ALSO connect one hand-wired MCP server at `start()`.\n */\nexport interface ConnectorMcpPluginOptions extends Partial<McpConnectorOptions> {\n /**\n * Policy for stdio transports on **declarative** `provider: 'mcp'`\n * instances (#3055). Default **deny**: metadata (including a runtime Studio\n * publish) must not spawn local processes unless the host opts in.\n * `string[]` allowlists specific commands; `true` allows any. Hand-wired\n * connectors configured via these plugin options are not subject to it —\n * their command lives in host code, not metadata.\n */\n declarativeStdio?: McpDeclarativeStdioPolicy;\n}\n\n/**\n * ConnectorMcpPlugin — contributes the generic MCP adapter (ADR-0024) in two forms:\n *\n * 1. **Provider factory** (`mcp`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'mcp'`\n * `connectors:` entries — connecting to the server and mapping its tools to\n * connector actions — at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `transport`, it also connects that one server at `start()` and registers\n * the resulting connector.\n *\n * Lifecycle: on `start()` a configured instance connects and builds the\n * connector once; on `destroy()` it tears the MCP connection down. If no\n * automation engine is present — or the server is unreachable at boot — the\n * hand-wired path logs and skips: a missing optional connector is not fatal\n * (unlike a *declarative* provider-bound instance, which fails boot loudly).\n */\nexport class ConnectorMcpPlugin implements Plugin {\n name = 'com.objectstack.connector.mcp';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector/provider registries) exist first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorMcpPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n private close?: () => Promise<void>;\n\n constructor(options: ConnectorMcpPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Contribute the `mcp` provider factory (ADR-0097) before the automation\n // service materializes declarative instances during its start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n MCP_PROVIDER_KEY,\n createMcpProviderFactory({\n clientFactory: this.options.clientFactory,\n declarativeStdio: this.options.declarativeStdio,\n }),\n );\n ctx.logger.info(\"ConnectorMcpPlugin: registered 'mcp' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Provider-only usage (no transport) contributes just the factory in init().\n if (!this.options.transport) return;\n\n const automation = this.tryGetAutomation(ctx);\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorMcpPlugin: no automation engine — MCP connector not registered');\n return;\n }\n\n let bundle;\n try {\n bundle = await createMcpConnector(this.options as McpConnectorOptions);\n } catch (err) {\n // The MCP server is unreachable / failed discovery at boot. Skip the\n // optional connector rather than failing the whole bootstrap.\n ctx.logger.warn(\n `ConnectorMcpPlugin: could not connect to MCP server — connector not registered: ${(err as Error).message}`,\n );\n return;\n }\n\n automation.registerConnector(bundle.def, bundle.handlers);\n this.automation = automation;\n this.connectorName = bundle.def.name;\n this.close = bundle.close;\n ctx.logger.info(\n `ConnectorMcpPlugin: MCP connector '${bundle.def.name}' registered with ${bundle.def.actions?.length ?? 0} action(s)`,\n );\n }\n\n /**\n * Destroy phase — the kernel's shutdown hook (the `Plugin` lifecycle exposes\n * `destroy()`, not `stop()`). Unregister the connector and tear the MCP\n * connection down so no child process / socket is leaked.\n */\n async destroy(): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n if (this.close) {\n try { await this.close(); } catch { /* ignore */ }\n }\n }\n\n private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -108,25 +108,128 @@ async function createMcpConnector(opts) {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// src/mcp-provider.ts
|
|
112
|
+
import { ConnectorUpstreamUnavailableError } from "@objectstack/spec/integration";
|
|
113
|
+
var MCP_PROVIDER_KEY = "mcp";
|
|
114
|
+
function isStringRecord(v) {
|
|
115
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
116
|
+
return Object.values(v).every((x) => typeof x === "string");
|
|
117
|
+
}
|
|
118
|
+
function applyAuthToHeaders(auth, headers) {
|
|
119
|
+
if (!auth || auth.type === "none") return;
|
|
120
|
+
switch (auth.type) {
|
|
121
|
+
case "bearer":
|
|
122
|
+
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
123
|
+
return;
|
|
124
|
+
case "basic":
|
|
125
|
+
headers["Authorization"] = `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString("base64")}`;
|
|
126
|
+
return;
|
|
127
|
+
case "api-key":
|
|
128
|
+
if (!auth.paramName) headers[auth.headerName ?? "X-API-Key"] = auth.key;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function normalizeTransport(raw, connectorName, auth) {
|
|
133
|
+
if (!raw || typeof raw !== "object") {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`connector-mcp provider: connector '${connectorName}' requires providerConfig.transport ({ kind: 'stdio', command, ... } or { kind: 'http', url, ... }).`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const t = raw;
|
|
139
|
+
if (t.kind === "stdio") {
|
|
140
|
+
if (typeof t.command !== "string" || t.command.length === 0) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`connector-mcp provider: connector '${connectorName}' stdio transport requires a 'command' string.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
kind: "stdio",
|
|
147
|
+
command: t.command,
|
|
148
|
+
args: Array.isArray(t.args) ? t.args.map((a) => String(a)) : void 0,
|
|
149
|
+
env: isStringRecord(t.env) ? t.env : void 0
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (t.kind === "http") {
|
|
153
|
+
if (typeof t.url !== "string" || t.url.length === 0) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`connector-mcp provider: connector '${connectorName}' http transport requires a 'url' string.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const headers = { ...isStringRecord(t.headers) ? t.headers : {} };
|
|
159
|
+
applyAuthToHeaders(auth, headers);
|
|
160
|
+
return { kind: "http", url: t.url, headers: Object.keys(headers).length > 0 ? headers : void 0 };
|
|
161
|
+
}
|
|
162
|
+
throw new Error(
|
|
163
|
+
`connector-mcp provider: connector '${connectorName}' providerConfig.transport.kind must be 'stdio' or 'http'.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
function assertDeclarativeStdioAllowed(policy, command, connectorName) {
|
|
167
|
+
if (policy === true) return;
|
|
168
|
+
if (Array.isArray(policy)) {
|
|
169
|
+
if (policy.includes(command)) return;
|
|
170
|
+
throw new Error(
|
|
171
|
+
`connector-mcp provider: connector '${connectorName}' declares a stdio transport with command '${command}', which is not in the host's declarativeStdio allowlist [${policy.join(", ")}]. Add the command to new ConnectorMcpPlugin({ declarativeStdio: [...] }) if this server is trusted (#3055).`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
throw new Error(
|
|
175
|
+
`connector-mcp provider: connector '${connectorName}' declares a stdio transport (command '${command}'), but declarative stdio transports are disabled by default \u2014 a stdio transport launches a local process from stack metadata (including runtime Studio publishes). If this server is trusted, opt in deliberately: new ConnectorMcpPlugin({ declarativeStdio: ['${command}'] }) \u2014 or use an http transport (#3055, ADR-0024 \xA74).`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
function createMcpProviderFactory(deps = {}) {
|
|
179
|
+
return async (ctx) => {
|
|
180
|
+
const cfg = ctx.providerConfig ?? {};
|
|
181
|
+
const transport = normalizeTransport(cfg.transport, ctx.name, ctx.auth);
|
|
182
|
+
if (transport.kind === "stdio") {
|
|
183
|
+
assertDeclarativeStdioAllowed(deps.declarativeStdio, transport.command, ctx.name);
|
|
184
|
+
}
|
|
185
|
+
const includeList = Array.isArray(cfg.include) ? cfg.include.filter((x) => typeof x === "string") : void 0;
|
|
186
|
+
const include = includeList ? (toolName) => includeList.includes(toolName) : void 0;
|
|
187
|
+
let bundle;
|
|
188
|
+
try {
|
|
189
|
+
bundle = await createMcpConnector({
|
|
190
|
+
name: ctx.name,
|
|
191
|
+
label: ctx.label,
|
|
192
|
+
description: ctx.description,
|
|
193
|
+
transport,
|
|
194
|
+
include,
|
|
195
|
+
clientFactory: deps.clientFactory
|
|
196
|
+
});
|
|
197
|
+
} catch (err) {
|
|
198
|
+
throw new ConnectorUpstreamUnavailableError(
|
|
199
|
+
`connector-mcp provider: connector '${ctx.name}' could not reach its MCP server: ${err.message}`,
|
|
200
|
+
{ cause: err }
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return { def: bundle.def, handlers: bundle.handlers, close: bundle.close };
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
111
207
|
// src/connector-mcp-plugin.ts
|
|
112
208
|
var ConnectorMcpPlugin = class {
|
|
113
|
-
constructor(options) {
|
|
209
|
+
constructor(options = {}) {
|
|
114
210
|
this.name = "com.objectstack.connector.mcp";
|
|
115
211
|
this.version = "1.0.0";
|
|
116
212
|
this.type = "standard";
|
|
117
|
-
// Ensure the automation engine (and its connector
|
|
213
|
+
// Ensure the automation engine (and its connector/provider registries) exist first.
|
|
118
214
|
this.dependencies = ["com.objectstack.service-automation"];
|
|
119
215
|
this.options = options;
|
|
120
216
|
}
|
|
121
|
-
async init(
|
|
217
|
+
async init(ctx) {
|
|
218
|
+
const automation = this.tryGetAutomation(ctx);
|
|
219
|
+
if (automation && typeof automation.registerConnectorProvider === "function") {
|
|
220
|
+
automation.registerConnectorProvider(
|
|
221
|
+
MCP_PROVIDER_KEY,
|
|
222
|
+
createMcpProviderFactory({
|
|
223
|
+
clientFactory: this.options.clientFactory,
|
|
224
|
+
declarativeStdio: this.options.declarativeStdio
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
ctx.logger.info("ConnectorMcpPlugin: registered 'mcp' connector provider");
|
|
228
|
+
}
|
|
122
229
|
}
|
|
123
230
|
async start(ctx) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
automation = ctx.getService("automation");
|
|
127
|
-
} catch {
|
|
128
|
-
automation = void 0;
|
|
129
|
-
}
|
|
231
|
+
if (!this.options.transport) return;
|
|
232
|
+
const automation = this.tryGetAutomation(ctx);
|
|
130
233
|
if (!automation || typeof automation.registerConnector !== "function") {
|
|
131
234
|
ctx.logger.info("ConnectorMcpPlugin: no automation engine \u2014 MCP connector not registered");
|
|
132
235
|
return;
|
|
@@ -167,9 +270,18 @@ var ConnectorMcpPlugin = class {
|
|
|
167
270
|
}
|
|
168
271
|
}
|
|
169
272
|
}
|
|
273
|
+
tryGetAutomation(ctx) {
|
|
274
|
+
try {
|
|
275
|
+
return ctx.getService("automation");
|
|
276
|
+
} catch {
|
|
277
|
+
return void 0;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
170
280
|
};
|
|
171
281
|
export {
|
|
172
282
|
ConnectorMcpPlugin,
|
|
173
|
-
|
|
283
|
+
MCP_PROVIDER_KEY,
|
|
284
|
+
createMcpConnector,
|
|
285
|
+
createMcpProviderFactory
|
|
174
286
|
};
|
|
175
287
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp-connector.ts","../src/connector-mcp-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * MCP connector — a *generic* adapter that turns any Model Context Protocol\n * server into a {@link Connector} (ADR-0024). Where `connector-rest` and\n * `connector-slack` are concrete, per-service connectors, this one is a single\n * adapter that adopts the entire MCP ecosystem with **no per-server code**:\n *\n * 1. connect to the MCP server over the configured transport,\n * 2. call `tools/list` and map each tool to a connector action\n * (`name → key`, `description → label/description`, `inputSchema → inputSchema`),\n * 3. build an ordinary `type: 'api'` {@link Connector} once, and\n * 4. dispatch each `connector_action` call to the server's `tools/call`.\n *\n * After construction the registry, the `connector_action` node, the discovery\n * route, and the Studio palette all see a plain connector — they never know it\n * is backed by MCP (ADR-0024 §2).\n *\n * **Credentials live with the MCP server, not in `ConnectorSchema`** (ADR-0024\n * §3). The operator supplies `env` (stdio) / `headers` (http) which we pass\n * straight to the transport; they are never copied into the serialized `def`\n * (which is exposed via discovery) and must never be logged.\n *\n * **Trust:** launching a stdio server runs a local process. Sandboxed,\n * multi-tenant execution and managed secrets are the enterprise tier (ADR-0024\n * §4); the open adapter runs an operator-provided server with operator-provided\n * credentials and documents that trust assumption.\n */\n\n/** How to reach the MCP server. */\nexport type McpTransport =\n | {\n kind: 'stdio';\n /** Executable to launch (e.g. `npx`). */\n command: string;\n /** Arguments passed to the command. */\n args?: string[];\n /** Environment variables for the child process — carries credentials. */\n env?: Record<string, string>;\n }\n | {\n kind: 'http';\n /** Streamable-HTTP endpoint of the MCP server. */\n url: string;\n /** Headers sent on every request — carries credentials (e.g. a bearer token). */\n headers?: Record<string, string>;\n };\n\n/** A tool as advertised by an MCP server's `tools/list`. */\nexport interface McpToolDescriptor {\n name: string;\n description?: string;\n /** JSON Schema for the tool's arguments. */\n inputSchema?: Record<string, unknown>;\n /** JSON Schema for the tool's result (optional — many servers omit it). */\n outputSchema?: Record<string, unknown>;\n}\n\n/**\n * The minimal slice of an MCP client the adapter needs. Kept structural so\n * tests can inject a fake and the real SDK stays an implementation detail\n * (mirrors `fetchImpl` injection in `connector-rest`).\n */\nexport interface McpClientLike {\n /** List the server's tools (`tools/list`). */\n listTools(): Promise<McpToolDescriptor[]>;\n /** Invoke a tool (`tools/call`); returns the raw MCP result. */\n callTool(name: string, args: Record<string, unknown>): Promise<unknown>;\n /** Close the connection / tear down the transport. */\n close(): Promise<void>;\n}\n\nexport interface McpConnectorOptions {\n /** Connector machine name (snake_case). Defaults to a slug of `label`, else `mcp`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Connector description for the palette. */\n description?: string;\n /** Icon identifier. Defaults to `plug`. */\n icon?: string;\n /** How to reach the MCP server. */\n transport: McpTransport;\n /** Only expose tools whose name matches (allowlist) — keeps the palette lean. */\n include?: (toolName: string) => boolean;\n /** Identifies this client to the MCP server during the handshake. */\n clientInfo?: { name: string; version: string };\n /**\n * Injected for tests; defaults to the real SDK-backed client. Receives the\n * configured transport and returns a connected {@link McpClientLike}.\n */\n clientFactory?: (transport: McpTransport, clientInfo: { name: string; version: string }) => Promise<McpClientLike>;\n}\n\n/**\n * A connector definition + handlers, ready for `engine.registerConnector()`,\n * plus a `close()` for the connection lifecycle (called by the plugin's stop()).\n */\nexport interface McpConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n /** Tear down the MCP client/connection. */\n close(): Promise<void>;\n}\n\nconst DEFAULT_CLIENT_INFO = { name: 'objectstack-connector-mcp', version: '1.0.0' } as const;\n\n/** Slugify a label into a valid connector `name` (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slugify(input: string): string {\n const slug = input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '');\n if (!slug) return 'mcp';\n // The name must start with a letter or underscore.\n return /^[a-z_]/.test(slug) ? slug : `mcp_${slug}`;\n}\n\n/** Title-case a snake_case name for a default label (`github_issues` → `Github Issues`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n\n/**\n * Normalise an MCP `tools/call` result into the connector handler's return\n * shape, mirroring the `{ ok, … }` envelope the other connectors expose. An MCP\n * result carries `content` blocks and an optional `isError` flag /\n * `structuredContent`; we surface `ok` from `isError` (never throwing on a\n * logical tool error so the flow author can branch on `${node.ok}`).\n */\nfunction normalizeResult(raw: unknown): Record<string, unknown> {\n const result = (raw ?? {}) as Record<string, unknown>;\n const isError = result.isError === true;\n const out: Record<string, unknown> = {\n ok: !isError,\n content: result.content ?? [],\n };\n if (result.structuredContent !== undefined) out.structuredContent = result.structuredContent;\n if (isError) out.isError = true;\n return out;\n}\n\n/**\n * Default per-request timeout (ms) for MCP calls (P1-1). Without it, a hung or\n * unresponsive MCP server stalls the agent turn indefinitely. The SDK aborts the\n * request once this elapses.\n */\nconst MCP_REQUEST_TIMEOUT_MS = 30_000;\n\n/**\n * The default {@link McpClientLike} — lazily imports the official MCP SDK so it\n * is only loaded when a real connection is made (tests inject their own client).\n */\nasync function defaultClientFactory(\n transport: McpTransport,\n clientInfo: { name: string; version: string },\n): Promise<McpClientLike> {\n const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');\n const client = new Client(clientInfo, { capabilities: {} });\n\n if (transport.kind === 'stdio') {\n const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');\n await client.connect(\n new StdioClientTransport({\n command: transport.command,\n args: transport.args,\n env: transport.env,\n }),\n );\n } else {\n const { StreamableHTTPClientTransport } = await import(\n '@modelcontextprotocol/sdk/client/streamableHttp.js'\n );\n await client.connect(\n new StreamableHTTPClientTransport(new URL(transport.url), {\n requestInit: transport.headers ? { headers: transport.headers } : undefined,\n }),\n );\n }\n\n return {\n async listTools() {\n const res = await client.listTools(undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });\n return (res.tools ?? []) as McpToolDescriptor[];\n },\n async callTool(name, args) {\n return client.callTool({ name, arguments: args }, undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });\n },\n async close() {\n await client.close();\n },\n };\n}\n\n/**\n * Connect to an MCP server, discover its tools, and build a {@link Connector}\n * whose actions dispatch to the server's `tools/call`. The connection is held\n * open for the lifetime of the bundle; call {@link McpConnectorBundle.close} to\n * tear it down.\n */\nexport async function createMcpConnector(opts: McpConnectorOptions): Promise<McpConnectorBundle> {\n const clientInfo = opts.clientInfo ?? DEFAULT_CLIENT_INFO;\n const factory = opts.clientFactory ?? defaultClientFactory;\n\n const client = await factory(opts.transport, clientInfo);\n\n let tools: McpToolDescriptor[];\n try {\n tools = await client.listTools();\n } catch (err) {\n // Discovery failed after connecting — release the connection rather than\n // leaking it, then surface the error to the caller (the plugin fail-soft).\n await client.close().catch(() => {});\n throw err;\n }\n\n const include = opts.include ?? (() => true);\n const selected = tools.filter((t) => include(t.name));\n\n const name = opts.name ?? slugify(opts.label ?? 'mcp');\n const label = opts.label ?? titleize(name);\n\n const handlers: McpConnectorBundle['handlers'] = {};\n const def: Connector = {\n name,\n label,\n type: 'api',\n description:\n opts.description ?? `MCP connector exposing ${selected.length} tool(s) from a Model Context Protocol server.`,\n icon: opts.icon ?? 'plug',\n // MCP servers own their own auth (passed via transport env/headers); we\n // do not model the upstream's credentials in ConnectorSchema (ADR-0024 §3).\n authentication: { type: 'none' },\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: selected.map((tool) => ({\n key: tool.name,\n // MCP tool names are machine names; derive a readable label and keep\n // the server's description verbatim (ADR-0024 `description → label/description`).\n label: titleize(slugify(tool.name)),\n description: tool.description,\n // The MCP inputSchema is already JSON Schema — pass it straight through.\n inputSchema: tool.inputSchema,\n // Many servers omit outputSchema; leave it unset when absent (as the\n // REST connector does for untyped responses).\n outputSchema: tool.outputSchema,\n })),\n };\n\n for (const tool of selected) {\n handlers[tool.name] = async (input) => normalizeResult(await client.callTool(tool.name, input));\n }\n\n return {\n def,\n handlers,\n close: () => client.close(),\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createMcpConnector, type McpConnectorOptions } from './mcp-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorMcpPluginOptions extends McpConnectorOptions {}\n\n/**\n * ConnectorMcpPlugin — connects to an MCP server, discovers its tools, and\n * registers them as a single connector on the automation engine (ADR-0024).\n * One generic adapter, configured per server (transport + `include`), never\n * per-server code.\n *\n * Lifecycle: on `start()` it connects and builds the connector once; on\n * `stop()` it tears the MCP connection down. If no automation engine is present\n * — or the server is unreachable at boot — the plugin logs and skips: a missing\n * optional connector is not a fatal error (same posture as `ConnectorRestPlugin`).\n */\nexport class ConnectorMcpPlugin implements Plugin {\n name = 'com.objectstack.connector.mcp';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorMcpPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n private close?: () => Promise<void>;\n\n constructor(options: ConnectorMcpPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available and the MCP server has been queried.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorMcpPlugin: no automation engine — MCP connector not registered');\n return;\n }\n\n let bundle;\n try {\n bundle = await createMcpConnector(this.options);\n } catch (err) {\n // The MCP server is unreachable / failed discovery at boot. Skip the\n // optional connector rather than failing the whole bootstrap.\n ctx.logger.warn(\n `ConnectorMcpPlugin: could not connect to MCP server — connector not registered: ${(err as Error).message}`,\n );\n return;\n }\n\n automation.registerConnector(bundle.def, bundle.handlers);\n this.automation = automation;\n this.connectorName = bundle.def.name;\n this.close = bundle.close;\n ctx.logger.info(\n `ConnectorMcpPlugin: MCP connector '${bundle.def.name}' registered with ${bundle.def.actions?.length ?? 0} action(s)`,\n );\n }\n\n /**\n * Destroy phase — the kernel's shutdown hook (the `Plugin` lifecycle exposes\n * `destroy()`, not `stop()`). Unregister the connector and tear the MCP\n * connection down so no child process / socket is leaked.\n */\n async destroy(): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n if (this.close) {\n try { await this.close(); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":";AA8GA,IAAM,sBAAsB,EAAE,MAAM,6BAA6B,SAAS,QAAQ;AAGlF,SAAS,QAAQ,OAAuB;AACpC,QAAM,OAAO,MACR,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE;AAC3B,MAAI,CAAC,KAAM,QAAO;AAElB,SAAO,UAAU,KAAK,IAAI,IAAI,OAAO,OAAO,IAAI;AACpD;AAGA,SAAS,SAAS,MAAsB;AACpC,SAAO,KACF,MAAM,GAAG,EACT,OAAO,OAAO,EACd,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,CAAC,EACjD,KAAK,GAAG;AACjB;AASA,SAAS,gBAAgB,KAAuC;AAC5D,QAAM,SAAU,OAAO,CAAC;AACxB,QAAM,UAAU,OAAO,YAAY;AACnC,QAAM,MAA+B;AAAA,IACjC,IAAI,CAAC;AAAA,IACL,SAAS,OAAO,WAAW,CAAC;AAAA,EAChC;AACA,MAAI,OAAO,sBAAsB,OAAW,KAAI,oBAAoB,OAAO;AAC3E,MAAI,QAAS,KAAI,UAAU;AAC3B,SAAO;AACX;AAOA,IAAM,yBAAyB;AAM/B,eAAe,qBACX,WACA,YACsB;AACtB,QAAM,EAAE,OAAO,IAAI,MAAM,OAAO,2CAA2C;AAC3E,QAAM,SAAS,IAAI,OAAO,YAAY,EAAE,cAAc,CAAC,EAAE,CAAC;AAE1D,MAAI,UAAU,SAAS,SAAS;AAC5B,UAAM,EAAE,qBAAqB,IAAI,MAAM,OAAO,2CAA2C;AACzF,UAAM,OAAO;AAAA,MACT,IAAI,qBAAqB;AAAA,QACrB,SAAS,UAAU;AAAA,QACnB,MAAM,UAAU;AAAA,QAChB,KAAK,UAAU;AAAA,MACnB,CAAC;AAAA,IACL;AAAA,EACJ,OAAO;AACH,UAAM,EAAE,8BAA8B,IAAI,MAAM,OAC5C,oDACJ;AACA,UAAM,OAAO;AAAA,MACT,IAAI,8BAA8B,IAAI,IAAI,UAAU,GAAG,GAAG;AAAA,QACtD,aAAa,UAAU,UAAU,EAAE,SAAS,UAAU,QAAQ,IAAI;AAAA,MACtE,CAAC;AAAA,IACL;AAAA,EACJ;AAEA,SAAO;AAAA,IACH,MAAM,YAAY;AACd,YAAM,MAAM,MAAM,OAAO,UAAU,QAAW,EAAE,SAAS,uBAAuB,CAAC;AACjF,aAAQ,IAAI,SAAS,CAAC;AAAA,IAC1B;AAAA,IACA,MAAM,SAAS,MAAM,MAAM;AACvB,aAAO,OAAO,SAAS,EAAE,MAAM,WAAW,KAAK,GAAG,QAAW,EAAE,SAAS,uBAAuB,CAAC;AAAA,IACpG;AAAA,IACA,MAAM,QAAQ;AACV,YAAM,OAAO,MAAM;AAAA,IACvB;AAAA,EACJ;AACJ;AAQA,eAAsB,mBAAmB,MAAwD;AAC7F,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,UAAU,KAAK,iBAAiB;AAEtC,QAAM,SAAS,MAAM,QAAQ,KAAK,WAAW,UAAU;AAEvD,MAAI;AACJ,MAAI;AACA,YAAQ,MAAM,OAAO,UAAU;AAAA,EACnC,SAAS,KAAK;AAGV,UAAM,OAAO,MAAM,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACnC,UAAM;AAAA,EACV;AAEA,QAAM,UAAU,KAAK,YAAY,MAAM;AACvC,QAAM,WAAW,MAAM,OAAO,CAAC,MAAM,QAAQ,EAAE,IAAI,CAAC;AAEpD,QAAM,OAAO,KAAK,QAAQ,QAAQ,KAAK,SAAS,KAAK;AACrD,QAAM,QAAQ,KAAK,SAAS,SAAS,IAAI;AAEzC,QAAM,WAA2C,CAAC;AAClD,QAAM,MAAiB;AAAA,IACnB;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN,aACI,KAAK,eAAe,0BAA0B,SAAS,MAAM;AAAA,IACjE,MAAM,KAAK,QAAQ;AAAA;AAAA;AAAA,IAGnB,gBAAgB,EAAE,MAAM,OAAO;AAAA;AAAA;AAAA,IAG/B,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,qBAAqB;AAAA,IACrB,kBAAkB;AAAA,IAClB,SAAS,SAAS,IAAI,CAAC,UAAU;AAAA,MAC7B,KAAK,KAAK;AAAA;AAAA;AAAA,MAGV,OAAO,SAAS,QAAQ,KAAK,IAAI,CAAC;AAAA,MAClC,aAAa,KAAK;AAAA;AAAA,MAElB,aAAa,KAAK;AAAA;AAAA;AAAA,MAGlB,cAAc,KAAK;AAAA,IACvB,EAAE;AAAA,EACN;AAEA,aAAW,QAAQ,UAAU;AACzB,aAAS,KAAK,IAAI,IAAI,OAAO,UAAU,gBAAgB,MAAM,OAAO,SAAS,KAAK,MAAM,KAAK,CAAC;AAAA,EAClG;AAEA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA,OAAO,MAAM,OAAO,MAAM;AAAA,EAC9B;AACJ;;;AC5OO,IAAM,qBAAN,MAA2C;AAAA,EAY9C,YAAY,SAAoC;AAXhD,gBAAO;AACP,mBAAU;AACV,gBAAO;AAEP;AAAA,wBAAe,CAAC,oCAAoC;AAQhD,SAAK,UAAU;AAAA,EACnB;AAAA,EAEA,MAAM,KAAK,MAAoC;AAAA,EAG/C;AAAA,EAEA,MAAM,MAAM,KAAmC;AAC3C,QAAI;AACJ,QAAI;AACA,mBAAa,IAAI,WAAqC,YAAY;AAAA,IACtE,QAAQ;AACJ,mBAAa;AAAA,IACjB;AAEA,QAAI,CAAC,cAAc,OAAO,WAAW,sBAAsB,YAAY;AACnE,UAAI,OAAO,KAAK,8EAAyE;AACzF;AAAA,IACJ;AAEA,QAAI;AACJ,QAAI;AACA,eAAS,MAAM,mBAAmB,KAAK,OAAO;AAAA,IAClD,SAAS,KAAK;AAGV,UAAI,OAAO;AAAA,QACP,wFAAoF,IAAc,OAAO;AAAA,MAC7G;AACA;AAAA,IACJ;AAEA,eAAW,kBAAkB,OAAO,KAAK,OAAO,QAAQ;AACxD,SAAK,aAAa;AAClB,SAAK,gBAAgB,OAAO,IAAI;AAChC,SAAK,QAAQ,OAAO;AACpB,QAAI,OAAO;AAAA,MACP,sCAAsC,OAAO,IAAI,IAAI,qBAAqB,OAAO,IAAI,SAAS,UAAU,CAAC;AAAA,IAC7G;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UAAyB;AAC3B,QAAI,KAAK,cAAc,KAAK,eAAe;AACvC,UAAI;AAAE,aAAK,WAAW,oBAAoB,KAAK,aAAa;AAAA,MAAG,QAAQ;AAAA,MAAe;AAAA,IAC1F;AACA,QAAI,KAAK,OAAO;AACZ,UAAI;AAAE,cAAM,KAAK,MAAM;AAAA,MAAG,QAAQ;AAAA,MAAe;AAAA,IACrD;AAAA,EACJ;AACJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/mcp-connector.ts","../src/mcp-provider.ts","../src/connector-mcp-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * MCP connector — a *generic* adapter that turns any Model Context Protocol\n * server into a {@link Connector} (ADR-0024). Where `connector-rest` and\n * `connector-slack` are concrete, per-service connectors, this one is a single\n * adapter that adopts the entire MCP ecosystem with **no per-server code**:\n *\n * 1. connect to the MCP server over the configured transport,\n * 2. call `tools/list` and map each tool to a connector action\n * (`name → key`, `description → label/description`, `inputSchema → inputSchema`),\n * 3. build an ordinary `type: 'api'` {@link Connector} once, and\n * 4. dispatch each `connector_action` call to the server's `tools/call`.\n *\n * After construction the registry, the `connector_action` node, the discovery\n * route, and the Studio palette all see a plain connector — they never know it\n * is backed by MCP (ADR-0024 §2).\n *\n * **Credentials live with the MCP server, not in `ConnectorSchema`** (ADR-0024\n * §3). The operator supplies `env` (stdio) / `headers` (http) which we pass\n * straight to the transport; they are never copied into the serialized `def`\n * (which is exposed via discovery) and must never be logged.\n *\n * **Trust:** launching a stdio server runs a local process. Sandboxed,\n * multi-tenant execution and managed secrets are the enterprise tier (ADR-0024\n * §4); the open adapter runs an operator-provided server with operator-provided\n * credentials and documents that trust assumption.\n */\n\n/** How to reach the MCP server. */\nexport type McpTransport =\n | {\n kind: 'stdio';\n /** Executable to launch (e.g. `npx`). */\n command: string;\n /** Arguments passed to the command. */\n args?: string[];\n /** Environment variables for the child process — carries credentials. */\n env?: Record<string, string>;\n }\n | {\n kind: 'http';\n /** Streamable-HTTP endpoint of the MCP server. */\n url: string;\n /** Headers sent on every request — carries credentials (e.g. a bearer token). */\n headers?: Record<string, string>;\n };\n\n/** A tool as advertised by an MCP server's `tools/list`. */\nexport interface McpToolDescriptor {\n name: string;\n description?: string;\n /** JSON Schema for the tool's arguments. */\n inputSchema?: Record<string, unknown>;\n /** JSON Schema for the tool's result (optional — many servers omit it). */\n outputSchema?: Record<string, unknown>;\n}\n\n/**\n * The minimal slice of an MCP client the adapter needs. Kept structural so\n * tests can inject a fake and the real SDK stays an implementation detail\n * (mirrors `fetchImpl` injection in `connector-rest`).\n */\nexport interface McpClientLike {\n /** List the server's tools (`tools/list`). */\n listTools(): Promise<McpToolDescriptor[]>;\n /** Invoke a tool (`tools/call`); returns the raw MCP result. */\n callTool(name: string, args: Record<string, unknown>): Promise<unknown>;\n /** Close the connection / tear down the transport. */\n close(): Promise<void>;\n}\n\nexport interface McpConnectorOptions {\n /** Connector machine name (snake_case). Defaults to a slug of `label`, else `mcp`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Connector description for the palette. */\n description?: string;\n /** Icon identifier. Defaults to `plug`. */\n icon?: string;\n /** How to reach the MCP server. */\n transport: McpTransport;\n /** Only expose tools whose name matches (allowlist) — keeps the palette lean. */\n include?: (toolName: string) => boolean;\n /** Identifies this client to the MCP server during the handshake. */\n clientInfo?: { name: string; version: string };\n /**\n * Injected for tests; defaults to the real SDK-backed client. Receives the\n * configured transport and returns a connected {@link McpClientLike}.\n */\n clientFactory?: (transport: McpTransport, clientInfo: { name: string; version: string }) => Promise<McpClientLike>;\n}\n\n/**\n * A connector definition + handlers, ready for `engine.registerConnector()`,\n * plus a `close()` for the connection lifecycle (called by the plugin's stop()).\n */\nexport interface McpConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n /** Tear down the MCP client/connection. */\n close(): Promise<void>;\n}\n\nconst DEFAULT_CLIENT_INFO = { name: 'objectstack-connector-mcp', version: '1.0.0' } as const;\n\n/** Slugify a label into a valid connector `name` (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slugify(input: string): string {\n const slug = input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '');\n if (!slug) return 'mcp';\n // The name must start with a letter or underscore.\n return /^[a-z_]/.test(slug) ? slug : `mcp_${slug}`;\n}\n\n/** Title-case a snake_case name for a default label (`github_issues` → `Github Issues`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n\n/**\n * Normalise an MCP `tools/call` result into the connector handler's return\n * shape, mirroring the `{ ok, … }` envelope the other connectors expose. An MCP\n * result carries `content` blocks and an optional `isError` flag /\n * `structuredContent`; we surface `ok` from `isError` (never throwing on a\n * logical tool error so the flow author can branch on `${node.ok}`).\n */\nfunction normalizeResult(raw: unknown): Record<string, unknown> {\n const result = (raw ?? {}) as Record<string, unknown>;\n const isError = result.isError === true;\n const out: Record<string, unknown> = {\n ok: !isError,\n content: result.content ?? [],\n };\n if (result.structuredContent !== undefined) out.structuredContent = result.structuredContent;\n if (isError) out.isError = true;\n return out;\n}\n\n/**\n * Default per-request timeout (ms) for MCP calls (P1-1). Without it, a hung or\n * unresponsive MCP server stalls the agent turn indefinitely. The SDK aborts the\n * request once this elapses.\n */\nconst MCP_REQUEST_TIMEOUT_MS = 30_000;\n\n/**\n * The default {@link McpClientLike} — lazily imports the official MCP SDK so it\n * is only loaded when a real connection is made (tests inject their own client).\n */\nasync function defaultClientFactory(\n transport: McpTransport,\n clientInfo: { name: string; version: string },\n): Promise<McpClientLike> {\n const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');\n const client = new Client(clientInfo, { capabilities: {} });\n\n if (transport.kind === 'stdio') {\n const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');\n await client.connect(\n new StdioClientTransport({\n command: transport.command,\n args: transport.args,\n env: transport.env,\n }),\n );\n } else {\n const { StreamableHTTPClientTransport } = await import(\n '@modelcontextprotocol/sdk/client/streamableHttp.js'\n );\n await client.connect(\n new StreamableHTTPClientTransport(new URL(transport.url), {\n requestInit: transport.headers ? { headers: transport.headers } : undefined,\n }),\n );\n }\n\n return {\n async listTools() {\n const res = await client.listTools(undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });\n return (res.tools ?? []) as McpToolDescriptor[];\n },\n async callTool(name, args) {\n return client.callTool({ name, arguments: args }, undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });\n },\n async close() {\n await client.close();\n },\n };\n}\n\n/**\n * Connect to an MCP server, discover its tools, and build a {@link Connector}\n * whose actions dispatch to the server's `tools/call`. The connection is held\n * open for the lifetime of the bundle; call {@link McpConnectorBundle.close} to\n * tear it down.\n */\nexport async function createMcpConnector(opts: McpConnectorOptions): Promise<McpConnectorBundle> {\n const clientInfo = opts.clientInfo ?? DEFAULT_CLIENT_INFO;\n const factory = opts.clientFactory ?? defaultClientFactory;\n\n const client = await factory(opts.transport, clientInfo);\n\n let tools: McpToolDescriptor[];\n try {\n tools = await client.listTools();\n } catch (err) {\n // Discovery failed after connecting — release the connection rather than\n // leaking it, then surface the error to the caller (the plugin fail-soft).\n await client.close().catch(() => {});\n throw err;\n }\n\n const include = opts.include ?? (() => true);\n const selected = tools.filter((t) => include(t.name));\n\n const name = opts.name ?? slugify(opts.label ?? 'mcp');\n const label = opts.label ?? titleize(name);\n\n const handlers: McpConnectorBundle['handlers'] = {};\n const def: Connector = {\n name,\n label,\n type: 'api',\n description:\n opts.description ?? `MCP connector exposing ${selected.length} tool(s) from a Model Context Protocol server.`,\n icon: opts.icon ?? 'plug',\n // MCP servers own their own auth (passed via transport env/headers); we\n // do not model the upstream's credentials in ConnectorSchema (ADR-0024 §3).\n authentication: { type: 'none' },\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: selected.map((tool) => ({\n key: tool.name,\n // MCP tool names are machine names; derive a readable label and keep\n // the server's description verbatim (ADR-0024 `description → label/description`).\n label: titleize(slugify(tool.name)),\n description: tool.description,\n // The MCP inputSchema is already JSON Schema — pass it straight through.\n inputSchema: tool.inputSchema,\n // Many servers omit outputSchema; leave it unset when absent (as the\n // REST connector does for untyped responses).\n outputSchema: tool.outputSchema,\n })),\n };\n\n for (const tool of selected) {\n handlers[tool.name] = async (input) => normalizeResult(await client.callTool(tool.name, input));\n }\n\n return {\n def,\n handlers,\n close: () => client.close(),\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectstack/spec/integration';\nimport { ConnectorUpstreamUnavailableError } from '@objectstack/spec/integration';\nimport { createMcpConnector, type McpConnectorOptions, type McpTransport } from './mcp-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'mcp'` is materialized by this factory.\n */\nexport const MCP_PROVIDER_KEY = 'mcp';\n\n/**\n * Host policy for **declarative** stdio transports (#3055). A stdio transport\n * launches a local child process, and declarative entries arrive through\n * metadata — including a runtime Studio publish — so spawning from them is\n * gated OFF by default:\n *\n * - `undefined` / `false` — deny (default): a `provider: 'mcp'` entry with a\n * stdio transport is rejected as a configuration fault.\n * - `string[]` — allowlist: the transport's `command` must strictly equal one\n * of the listed commands. NOTE this is a coarse trust boundary — listing a\n * launcher like `npx` effectively allows any package it can run; list the\n * specific server binaries you trust. Sandboxed execution is the enterprise\n * tier (ADR-0024 §4).\n * - `true` — allow any command (explicit full trust; hosts that treat every\n * metadata author as an operator).\n *\n * Hand-wired connectors (plugin instance options / `createMcpConnector`) are\n * NOT subject to this policy: their command was written in host code, a\n * different trust anchor than metadata.\n */\nexport type McpDeclarativeStdioPolicy = boolean | string[];\n\n/** Injectable dependencies for {@link createMcpProviderFactory} (tests). */\nexport interface McpProviderDeps {\n /** Injected MCP client factory; defaults to the SDK-backed client. */\n clientFactory?: McpConnectorOptions['clientFactory'];\n /** Policy for declarative stdio transports (#3055). Default: deny. */\n declarativeStdio?: McpDeclarativeStdioPolicy;\n}\n\n/** Shape of `providerConfig` for a `provider: 'mcp'` declarative instance. */\ninterface McpProviderConfig {\n /** How to reach the MCP server (stdio or streamable-http). */\n transport?: unknown;\n /** Optional tool-name allowlist — only these tools become actions. */\n include?: unknown;\n}\n\nfunction isStringRecord(v: unknown): v is Record<string, string> {\n if (!v || typeof v !== 'object' || Array.isArray(v)) return false;\n return Object.values(v as Record<string, unknown>).every((x) => typeof x === 'string');\n}\n\n/**\n * Fold the resolved instance `auth` into an MCP **http** transport's headers\n * (ADR-0024 keeps MCP credentials with the transport). `credentialRef` has\n * already been resolved upstream, so this only maps the static credential to the\n * right header. Not applied to stdio transports — a stdio server receives its\n * credentials through `transport.env`.\n */\nfunction applyAuthToHeaders(\n auth: ResolvedConnectorAuth | undefined,\n headers: Record<string, string>,\n): void {\n if (!auth || auth.type === 'none') return;\n switch (auth.type) {\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic':\n headers['Authorization'] = `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`;\n return;\n case 'api-key':\n // Header-based only for MCP http (query-param keys are not part of the transport).\n if (!auth.paramName) headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\n/** Validate + normalize `providerConfig.transport`, injecting resolved auth for http. */\nfunction normalizeTransport(\n raw: unknown,\n connectorName: string,\n auth: ResolvedConnectorAuth | undefined,\n): McpTransport {\n if (!raw || typeof raw !== 'object') {\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' requires providerConfig.transport ` +\n `({ kind: 'stdio', command, ... } or { kind: 'http', url, ... }).`,\n );\n }\n const t = raw as Record<string, unknown>;\n if (t.kind === 'stdio') {\n if (typeof t.command !== 'string' || t.command.length === 0) {\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' stdio transport requires a 'command' string.`,\n );\n }\n return {\n kind: 'stdio',\n command: t.command,\n args: Array.isArray(t.args) ? t.args.map((a) => String(a)) : undefined,\n env: isStringRecord(t.env) ? t.env : undefined,\n };\n }\n if (t.kind === 'http') {\n if (typeof t.url !== 'string' || t.url.length === 0) {\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' http transport requires a 'url' string.`,\n );\n }\n const headers: Record<string, string> = { ...(isStringRecord(t.headers) ? t.headers : {}) };\n applyAuthToHeaders(auth, headers);\n return { kind: 'http', url: t.url, headers: Object.keys(headers).length > 0 ? headers : undefined };\n }\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' providerConfig.transport.kind must be 'stdio' or 'http'.`,\n );\n}\n\n/**\n * Enforce the {@link McpDeclarativeStdioPolicy} for one declarative instance\n * (#3055). Throws a **plain** Error on violation: a security-policy rejection\n * is a configuration fault — fatal at boot, skipped+logged on reload — and must\n * never be classified upstream-unavailable (it cannot be retried into\n * existence).\n */\nfunction assertDeclarativeStdioAllowed(\n policy: McpDeclarativeStdioPolicy | undefined,\n command: string,\n connectorName: string,\n): void {\n if (policy === true) return;\n if (Array.isArray(policy)) {\n if (policy.includes(command)) return;\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' declares a stdio transport with command '${command}', ` +\n `which is not in the host's declarativeStdio allowlist [${policy.join(', ')}]. ` +\n `Add the command to new ConnectorMcpPlugin({ declarativeStdio: [...] }) if this server is trusted (#3055).`,\n );\n }\n throw new Error(\n `connector-mcp provider: connector '${connectorName}' declares a stdio transport (command '${command}'), ` +\n `but declarative stdio transports are disabled by default — a stdio transport launches a local process ` +\n `from stack metadata (including runtime Studio publishes). If this server is trusted, opt in deliberately: ` +\n `new ConnectorMcpPlugin({ declarativeStdio: ['${command}'] }) — or use an http transport (#3055, ADR-0024 §4).`,\n );\n}\n\n/**\n * Build the `mcp` {@link ConnectorProviderFactory} (ADR-0097 / ADR-0024). At boot\n * the automation service invokes it for each `provider: 'mcp'` declarative\n * instance: it connects to the MCP server named by `providerConfig.transport`,\n * lists its tools, and produces the same `{ def, handlers, close }` bundle\n * {@link createMcpConnector} builds for a hand-wired MCP connector — one action\n * per tool, dispatched to the server's `tools/call`.\n *\n * Stdio transports on declarative instances are policy-gated (default deny) —\n * see {@link McpDeclarativeStdioPolicy} (#3055).\n *\n * The connection is opened at materialization. Faults are classified (#3017):\n * an invalid transport shape is a *configuration* fault and throws plain —\n * fatal at boot per the ADR-0097 fail-loud contract — while a connect /\n * `tools/list` failure (server down, refused, timed out) is an *operational*\n * fault and throws {@link ConnectorUpstreamUnavailableError}, which the\n * materializer turns into a degraded instance that is retried with backoff\n * instead of aborting the whole app boot.\n */\nexport function createMcpProviderFactory(deps: McpProviderDeps = {}): ConnectorProviderFactory {\n return async (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as McpProviderConfig;\n const transport = normalizeTransport(cfg.transport, ctx.name, ctx.auth);\n if (transport.kind === 'stdio') {\n assertDeclarativeStdioAllowed(deps.declarativeStdio, transport.command, ctx.name);\n }\n const includeList = Array.isArray(cfg.include)\n ? cfg.include.filter((x): x is string => typeof x === 'string')\n : undefined;\n const include = includeList ? (toolName: string) => includeList.includes(toolName) : undefined;\n\n let bundle;\n try {\n bundle = await createMcpConnector({\n name: ctx.name,\n label: ctx.label,\n description: ctx.description,\n transport,\n include,\n clientFactory: deps.clientFactory,\n });\n } catch (err) {\n // Everything past transport validation is talking to the server (connect,\n // handshake, tools/list) — operational, hence retryable. A credential the\n // server rejects also lands here: indistinguishable from the outside, and\n // retrying it is loud (logged per attempt), never silent.\n throw new ConnectorUpstreamUnavailableError(\n `connector-mcp provider: connector '${ctx.name}' could not reach its MCP server: ${(err as Error).message}`,\n { cause: err },\n );\n }\n return { def: bundle.def, handlers: bundle.handlers, close: bundle.close };\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createMcpConnector, type McpConnectorOptions } from './mcp-connector.js';\nimport {\n createMcpProviderFactory,\n MCP_PROVIDER_KEY,\n type McpDeclarativeStdioPolicy,\n} from './mcp-provider.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so the plugin needs no runtime dependency on\n * `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Options for {@link ConnectorMcpPlugin}. All optional (ADR-0097): with no\n * `transport` the plugin contributes only the `mcp` provider factory — so a\n * stack can declare `provider: 'mcp'` instances as pure metadata. Supply a\n * `transport` to ALSO connect one hand-wired MCP server at `start()`.\n */\nexport interface ConnectorMcpPluginOptions extends Partial<McpConnectorOptions> {\n /**\n * Policy for stdio transports on **declarative** `provider: 'mcp'`\n * instances (#3055). Default **deny**: metadata (including a runtime Studio\n * publish) must not spawn local processes unless the host opts in.\n * `string[]` allowlists specific commands; `true` allows any. Hand-wired\n * connectors configured via these plugin options are not subject to it —\n * their command lives in host code, not metadata.\n */\n declarativeStdio?: McpDeclarativeStdioPolicy;\n}\n\n/**\n * ConnectorMcpPlugin — contributes the generic MCP adapter (ADR-0024) in two forms:\n *\n * 1. **Provider factory** (`mcp`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'mcp'`\n * `connectors:` entries — connecting to the server and mapping its tools to\n * connector actions — at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `transport`, it also connects that one server at `start()` and registers\n * the resulting connector.\n *\n * Lifecycle: on `start()` a configured instance connects and builds the\n * connector once; on `destroy()` it tears the MCP connection down. If no\n * automation engine is present — or the server is unreachable at boot — the\n * hand-wired path logs and skips: a missing optional connector is not fatal\n * (unlike a *declarative* provider-bound instance, which fails boot loudly).\n */\nexport class ConnectorMcpPlugin implements Plugin {\n name = 'com.objectstack.connector.mcp';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector/provider registries) exist first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorMcpPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n private close?: () => Promise<void>;\n\n constructor(options: ConnectorMcpPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Contribute the `mcp` provider factory (ADR-0097) before the automation\n // service materializes declarative instances during its start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n MCP_PROVIDER_KEY,\n createMcpProviderFactory({\n clientFactory: this.options.clientFactory,\n declarativeStdio: this.options.declarativeStdio,\n }),\n );\n ctx.logger.info(\"ConnectorMcpPlugin: registered 'mcp' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Provider-only usage (no transport) contributes just the factory in init().\n if (!this.options.transport) return;\n\n const automation = this.tryGetAutomation(ctx);\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorMcpPlugin: no automation engine — MCP connector not registered');\n return;\n }\n\n let bundle;\n try {\n bundle = await createMcpConnector(this.options as McpConnectorOptions);\n } catch (err) {\n // The MCP server is unreachable / failed discovery at boot. Skip the\n // optional connector rather than failing the whole bootstrap.\n ctx.logger.warn(\n `ConnectorMcpPlugin: could not connect to MCP server — connector not registered: ${(err as Error).message}`,\n );\n return;\n }\n\n automation.registerConnector(bundle.def, bundle.handlers);\n this.automation = automation;\n this.connectorName = bundle.def.name;\n this.close = bundle.close;\n ctx.logger.info(\n `ConnectorMcpPlugin: MCP connector '${bundle.def.name}' registered with ${bundle.def.actions?.length ?? 0} action(s)`,\n );\n }\n\n /**\n * Destroy phase — the kernel's shutdown hook (the `Plugin` lifecycle exposes\n * `destroy()`, not `stop()`). Unregister the connector and tear the MCP\n * connection down so no child process / socket is leaked.\n */\n async destroy(): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n if (this.close) {\n try { await this.close(); } catch { /* ignore */ }\n }\n }\n\n private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/connector-mcp",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.1.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Model Context Protocol (MCP) connector for ObjectStack — a generic adapter that turns any MCP server's tools into a connector's actions on the automation engine's connector registry (ADR-0024).",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
17
|
-
"@objectstack/core": "15.
|
|
18
|
-
"@objectstack/spec": "15.
|
|
17
|
+
"@objectstack/core": "15.1.1",
|
|
18
|
+
"@objectstack/spec": "15.1.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^26.1.1",
|
|
22
22
|
"typescript": "^6.0.3",
|
|
23
23
|
"vitest": "^4.1.10",
|
|
24
|
-
"@objectstack/service-automation": "15.
|
|
24
|
+
"@objectstack/service-automation": "15.1.1"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"objectstack",
|
|
@@ -41,6 +41,9 @@ describe('ConnectorMcpPlugin — end to end with the automation engine', () => {
|
|
|
41
41
|
kernel.use(new AutomationServicePlugin());
|
|
42
42
|
kernel.use(
|
|
43
43
|
new ConnectorMcpPlugin({
|
|
44
|
+
// NOTE deliberately no `declarativeStdio`: the #3055 default-deny
|
|
45
|
+
// policy gates DECLARATIVE instances only — this hand-wired stdio
|
|
46
|
+
// transport (host code, not metadata) must keep working un-gated.
|
|
44
47
|
name: 'github_mcp',
|
|
45
48
|
label: 'GitHub MCP',
|
|
46
49
|
transport: { kind: 'stdio', command: 'noop' },
|
|
@@ -93,4 +96,32 @@ describe('ConnectorMcpPlugin — end to end with the automation engine', () => {
|
|
|
93
96
|
await kernel.shutdown();
|
|
94
97
|
expect(isClosed()).toBe(true);
|
|
95
98
|
});
|
|
99
|
+
|
|
100
|
+
it('plumbs declarativeStdio through to the registered provider factory (#3055)', async () => {
|
|
101
|
+
const { client } = fakeClient();
|
|
102
|
+
let registered: ((ctx: unknown) => Promise<unknown>) | undefined;
|
|
103
|
+
const automationStub = {
|
|
104
|
+
registerConnector: () => {},
|
|
105
|
+
unregisterConnector: () => {},
|
|
106
|
+
registerConnectorProvider: (_key: string, factory: never) => { registered = factory; },
|
|
107
|
+
};
|
|
108
|
+
const plugin = new ConnectorMcpPlugin({
|
|
109
|
+
clientFactory: async () => client,
|
|
110
|
+
declarativeStdio: ['trusted-mcp'],
|
|
111
|
+
});
|
|
112
|
+
await plugin.init({
|
|
113
|
+
getService: () => automationStub,
|
|
114
|
+
logger: { info: () => {}, warn: () => {} },
|
|
115
|
+
} as never);
|
|
116
|
+
expect(registered).toBeDefined();
|
|
117
|
+
|
|
118
|
+
const provider = registered!;
|
|
119
|
+
const declarativeCtx = (command: string) => ({
|
|
120
|
+
name: 'x', label: 'X', type: 'api',
|
|
121
|
+
providerConfig: { transport: { kind: 'stdio', command } },
|
|
122
|
+
});
|
|
123
|
+
// Allowlisted command materializes; anything else is denied by policy.
|
|
124
|
+
await expect(provider(declarativeCtx('trusted-mcp'))).resolves.toBeDefined();
|
|
125
|
+
await expect(provider(declarativeCtx('bash'))).rejects.toThrow(/declarativeStdio allowlist/);
|
|
126
|
+
});
|
|
96
127
|
});
|