@objectstack/connector-mcp 7.8.0 → 8.0.0

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/connector-mcp@7.8.0 build /home/runner/work/framework/framework/packages/connectors/connector-mcp
2
+ > @objectstack/connector-mcp@8.0.0 build /home/runner/work/framework/framework/packages/connectors/connector-mcp
3
3
  > tsup --config ../../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- CJS dist/index.js 7.33 KB
14
- CJS dist/index.js.map 19.61 KB
15
- CJS ⚡️ Build success in 86ms
16
- ESM dist/index.mjs 5.69 KB
17
- ESM dist/index.mjs.map 18.22 KB
18
- ESM ⚡️ Build success in 87ms
13
+ ESM dist/index.mjs 5.81 KB
14
+ ESM dist/index.mjs.map 18.62 KB
15
+ ESM ⚡️ Build success in 65ms
16
+ CJS dist/index.js 7.45 KB
17
+ CJS dist/index.js.map 20.01 KB
18
+ CJS ⚡️ Build success in 77ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 13713ms
20
+ DTS ⚡️ Build success in 14550ms
21
21
  DTS dist/index.d.mts 6.71 KB
22
22
  DTS dist/index.d.ts 6.71 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # @objectstack/connector-mcp
2
2
 
3
+ ## 8.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - d5a8161: feat(spec): resilientFetch — timeout + backoff for outbound HTTP (P1-1)
8
+
9
+ Outbound calls in the connectors/embedder were naked `fetch` with no timeout or
10
+ retry, so a slow or rate-limited external API could hang an agent turn with no
11
+ recovery.
12
+
13
+ New shared `resilientFetch` (`@objectstack/spec/shared`):
14
+
15
+ - per-attempt timeout via `AbortController` (default 30s);
16
+ - exponential backoff with jitter, up to 3 attempts, on network errors / 429 / 5xx;
17
+ - honours a `Retry-After` header on 429;
18
+ - never retries a caller-initiated abort (intentional cancellation).
19
+
20
+ Wired into `connector-rest`, `connector-slack`, and `embedder-openai`.
21
+ `connector-mcp` talks through the MCP SDK transport, so it gets a 30s per-request
22
+ `timeout` on `callTool` / `listTools` instead.
23
+
24
+ A stateful per-host **circuit breaker** is deliberately left as a follow-up:
25
+ timeout + backoff already removes the hang/no-recovery risk.
26
+
27
+ - Updated dependencies [a46c017]
28
+ - Updated dependencies [b990b89]
29
+ - Updated dependencies [99111ec]
30
+ - Updated dependencies [d5a8161]
31
+ - Updated dependencies [5cf1f1b]
32
+ - Updated dependencies [9ef89d4]
33
+ - Updated dependencies [3306d2f]
34
+ - Updated dependencies [c262301]
35
+ - Updated dependencies [bc44195]
36
+ - Updated dependencies [9e2e229]
37
+ - @objectstack/spec@8.0.0
38
+ - @objectstack/core@8.0.0
39
+
40
+ ## 7.9.0
41
+
42
+ ### Patch Changes
43
+
44
+ - @objectstack/spec@7.9.0
45
+ - @objectstack/core@7.9.0
46
+
3
47
  ## 7.8.0
4
48
 
5
49
  ### Patch Changes
package/dist/index.js CHANGED
@@ -56,6 +56,7 @@ function normalizeResult(raw) {
56
56
  if (isError) out.isError = true;
57
57
  return out;
58
58
  }
59
+ var MCP_REQUEST_TIMEOUT_MS = 3e4;
59
60
  async function defaultClientFactory(transport, clientInfo) {
60
61
  const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
61
62
  const client = new Client(clientInfo, { capabilities: {} });
@@ -78,11 +79,11 @@ async function defaultClientFactory(transport, clientInfo) {
78
79
  }
79
80
  return {
80
81
  async listTools() {
81
- const res = await client.listTools();
82
+ const res = await client.listTools(void 0, { timeout: MCP_REQUEST_TIMEOUT_MS });
82
83
  return res.tools ?? [];
83
84
  },
84
85
  async callTool(name, args) {
85
- return client.callTool({ name, arguments: args });
86
+ return client.callTool({ name, arguments: args }, void 0, { timeout: MCP_REQUEST_TIMEOUT_MS });
86
87
  },
87
88
  async close() {
88
89
  await client.close();
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 * 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();\n return (res.tools ?? []) as McpToolDescriptor[];\n },\n async callTool(name, args) {\n return client.callTool({ name, arguments: args });\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/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":[]}
package/dist/index.mjs CHANGED
@@ -19,6 +19,7 @@ function normalizeResult(raw) {
19
19
  if (isError) out.isError = true;
20
20
  return out;
21
21
  }
22
+ var MCP_REQUEST_TIMEOUT_MS = 3e4;
22
23
  async function defaultClientFactory(transport, clientInfo) {
23
24
  const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
24
25
  const client = new Client(clientInfo, { capabilities: {} });
@@ -41,11 +42,11 @@ async function defaultClientFactory(transport, clientInfo) {
41
42
  }
42
43
  return {
43
44
  async listTools() {
44
- const res = await client.listTools();
45
+ const res = await client.listTools(void 0, { timeout: MCP_REQUEST_TIMEOUT_MS });
45
46
  return res.tools ?? [];
46
47
  },
47
48
  async callTool(name, args) {
48
- return client.callTool({ name, arguments: args });
49
+ return client.callTool({ name, arguments: args }, void 0, { timeout: MCP_REQUEST_TIMEOUT_MS });
49
50
  },
50
51
  async close() {
51
52
  await client.close();
@@ -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 * 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();\n return (res.tools ?? []) as McpToolDescriptor[];\n },\n async callTool(name, args) {\n return client.callTool({ name, arguments: args });\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/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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/connector-mcp",
3
- "version": "7.8.0",
3
+ "version": "8.0.0",
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": "7.8.0",
18
- "@objectstack/spec": "7.8.0"
17
+ "@objectstack/core": "8.0.0",
18
+ "@objectstack/spec": "8.0.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^25.9.1",
22
22
  "typescript": "^6.0.3",
23
23
  "vitest": "^4.1.8",
24
- "@objectstack/service-automation": "7.8.0"
24
+ "@objectstack/service-automation": "8.0.0"
25
25
  },
26
26
  "keywords": [
27
27
  "objectstack",
@@ -149,6 +149,13 @@ function normalizeResult(raw: unknown): Record<string, unknown> {
149
149
  return out;
150
150
  }
151
151
 
152
+ /**
153
+ * Default per-request timeout (ms) for MCP calls (P1-1). Without it, a hung or
154
+ * unresponsive MCP server stalls the agent turn indefinitely. The SDK aborts the
155
+ * request once this elapses.
156
+ */
157
+ const MCP_REQUEST_TIMEOUT_MS = 30_000;
158
+
152
159
  /**
153
160
  * The default {@link McpClientLike} — lazily imports the official MCP SDK so it
154
161
  * is only loaded when a real connection is made (tests inject their own client).
@@ -182,11 +189,11 @@ async function defaultClientFactory(
182
189
 
183
190
  return {
184
191
  async listTools() {
185
- const res = await client.listTools();
192
+ const res = await client.listTools(undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });
186
193
  return (res.tools ?? []) as McpToolDescriptor[];
187
194
  },
188
195
  async callTool(name, args) {
189
- return client.callTool({ name, arguments: args });
196
+ return client.callTool({ name, arguments: args }, undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });
190
197
  },
191
198
  async close() {
192
199
  await client.close();