@objectstack/connector-mcp 7.4.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.
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC8GA,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;AAMA,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;AACnC,aAAQ,IAAI,SAAS,CAAC;AAAA,IAC1B;AAAA,IACA,MAAM,SAAS,MAAM,MAAM;AACvB,aAAO,OAAO,SAAS,EAAE,MAAM,WAAW,KAAK,CAAC;AAAA,IACpD;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;;;ACrOO,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/dist/index.mjs ADDED
@@ -0,0 +1,174 @@
1
+ // src/mcp-connector.ts
2
+ var DEFAULT_CLIENT_INFO = { name: "objectstack-connector-mcp", version: "1.0.0" };
3
+ function slugify(input) {
4
+ const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
5
+ if (!slug) return "mcp";
6
+ return /^[a-z_]/.test(slug) ? slug : `mcp_${slug}`;
7
+ }
8
+ function titleize(name) {
9
+ return name.split("_").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
10
+ }
11
+ function normalizeResult(raw) {
12
+ const result = raw ?? {};
13
+ const isError = result.isError === true;
14
+ const out = {
15
+ ok: !isError,
16
+ content: result.content ?? []
17
+ };
18
+ if (result.structuredContent !== void 0) out.structuredContent = result.structuredContent;
19
+ if (isError) out.isError = true;
20
+ return out;
21
+ }
22
+ async function defaultClientFactory(transport, clientInfo) {
23
+ const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
24
+ const client = new Client(clientInfo, { capabilities: {} });
25
+ if (transport.kind === "stdio") {
26
+ const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
27
+ await client.connect(
28
+ new StdioClientTransport({
29
+ command: transport.command,
30
+ args: transport.args,
31
+ env: transport.env
32
+ })
33
+ );
34
+ } else {
35
+ const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
36
+ await client.connect(
37
+ new StreamableHTTPClientTransport(new URL(transport.url), {
38
+ requestInit: transport.headers ? { headers: transport.headers } : void 0
39
+ })
40
+ );
41
+ }
42
+ return {
43
+ async listTools() {
44
+ const res = await client.listTools();
45
+ return res.tools ?? [];
46
+ },
47
+ async callTool(name, args) {
48
+ return client.callTool({ name, arguments: args });
49
+ },
50
+ async close() {
51
+ await client.close();
52
+ }
53
+ };
54
+ }
55
+ async function createMcpConnector(opts) {
56
+ const clientInfo = opts.clientInfo ?? DEFAULT_CLIENT_INFO;
57
+ const factory = opts.clientFactory ?? defaultClientFactory;
58
+ const client = await factory(opts.transport, clientInfo);
59
+ let tools;
60
+ try {
61
+ tools = await client.listTools();
62
+ } catch (err) {
63
+ await client.close().catch(() => {
64
+ });
65
+ throw err;
66
+ }
67
+ const include = opts.include ?? (() => true);
68
+ const selected = tools.filter((t) => include(t.name));
69
+ const name = opts.name ?? slugify(opts.label ?? "mcp");
70
+ const label = opts.label ?? titleize(name);
71
+ const handlers = {};
72
+ const def = {
73
+ name,
74
+ label,
75
+ type: "api",
76
+ description: opts.description ?? `MCP connector exposing ${selected.length} tool(s) from a Model Context Protocol server.`,
77
+ icon: opts.icon ?? "plug",
78
+ // MCP servers own their own auth (passed via transport env/headers); we
79
+ // do not model the upstream's credentials in ConnectorSchema (ADR-0024 §3).
80
+ authentication: { type: "none" },
81
+ // Defaulted by ConnectorSchema; set explicitly so the literal satisfies
82
+ // the (post-parse) Connector output type.
83
+ status: "active",
84
+ enabled: true,
85
+ connectionTimeoutMs: 3e4,
86
+ requestTimeoutMs: 3e4,
87
+ actions: selected.map((tool) => ({
88
+ key: tool.name,
89
+ // MCP tool names are machine names; derive a readable label and keep
90
+ // the server's description verbatim (ADR-0024 `description → label/description`).
91
+ label: titleize(slugify(tool.name)),
92
+ description: tool.description,
93
+ // The MCP inputSchema is already JSON Schema — pass it straight through.
94
+ inputSchema: tool.inputSchema,
95
+ // Many servers omit outputSchema; leave it unset when absent (as the
96
+ // REST connector does for untyped responses).
97
+ outputSchema: tool.outputSchema
98
+ }))
99
+ };
100
+ for (const tool of selected) {
101
+ handlers[tool.name] = async (input) => normalizeResult(await client.callTool(tool.name, input));
102
+ }
103
+ return {
104
+ def,
105
+ handlers,
106
+ close: () => client.close()
107
+ };
108
+ }
109
+
110
+ // src/connector-mcp-plugin.ts
111
+ var ConnectorMcpPlugin = class {
112
+ constructor(options) {
113
+ this.name = "com.objectstack.connector.mcp";
114
+ this.version = "1.0.0";
115
+ this.type = "standard";
116
+ // Ensure the automation engine (and its connector registry) is started first.
117
+ this.dependencies = ["com.objectstack.service-automation"];
118
+ this.options = options;
119
+ }
120
+ async init(_ctx) {
121
+ }
122
+ async start(ctx) {
123
+ let automation;
124
+ try {
125
+ automation = ctx.getService("automation");
126
+ } catch {
127
+ automation = void 0;
128
+ }
129
+ if (!automation || typeof automation.registerConnector !== "function") {
130
+ ctx.logger.info("ConnectorMcpPlugin: no automation engine \u2014 MCP connector not registered");
131
+ return;
132
+ }
133
+ let bundle;
134
+ try {
135
+ bundle = await createMcpConnector(this.options);
136
+ } catch (err) {
137
+ ctx.logger.warn(
138
+ `ConnectorMcpPlugin: could not connect to MCP server \u2014 connector not registered: ${err.message}`
139
+ );
140
+ return;
141
+ }
142
+ automation.registerConnector(bundle.def, bundle.handlers);
143
+ this.automation = automation;
144
+ this.connectorName = bundle.def.name;
145
+ this.close = bundle.close;
146
+ ctx.logger.info(
147
+ `ConnectorMcpPlugin: MCP connector '${bundle.def.name}' registered with ${bundle.def.actions?.length ?? 0} action(s)`
148
+ );
149
+ }
150
+ /**
151
+ * Destroy phase — the kernel's shutdown hook (the `Plugin` lifecycle exposes
152
+ * `destroy()`, not `stop()`). Unregister the connector and tear the MCP
153
+ * connection down so no child process / socket is leaked.
154
+ */
155
+ async destroy() {
156
+ if (this.automation && this.connectorName) {
157
+ try {
158
+ this.automation.unregisterConnector(this.connectorName);
159
+ } catch {
160
+ }
161
+ }
162
+ if (this.close) {
163
+ try {
164
+ await this.close();
165
+ } catch {
166
+ }
167
+ }
168
+ }
169
+ };
170
+ export {
171
+ ConnectorMcpPlugin,
172
+ createMcpConnector
173
+ };
174
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@objectstack/connector-mcp",
3
+ "version": "7.4.0",
4
+ "license": "Apache-2.0",
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
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.29.0",
17
+ "@objectstack/core": "7.4.0",
18
+ "@objectstack/spec": "7.4.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^25.9.1",
22
+ "typescript": "^6.0.3",
23
+ "vitest": "^4.1.7",
24
+ "@objectstack/service-automation": "7.4.0"
25
+ },
26
+ "keywords": [
27
+ "objectstack",
28
+ "connector",
29
+ "mcp",
30
+ "model-context-protocol",
31
+ "integration",
32
+ "ai",
33
+ "tools"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsup --config ../../../tsup.config.ts",
37
+ "test": "vitest run --passWithNoTests"
38
+ }
39
+ }
@@ -0,0 +1,96 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { describe, it, expect } from 'vitest';
4
+ import { LiteKernel } from '@objectstack/core';
5
+ import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/service-automation';
6
+ import { ConnectorMcpPlugin } from './connector-mcp-plugin.js';
7
+ import type { McpClientLike, McpToolDescriptor } from './mcp-connector.js';
8
+
9
+ const TOOLS: McpToolDescriptor[] = [
10
+ {
11
+ name: 'create_issue',
12
+ description: 'Create a GitHub issue',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: { repo: { type: 'string' }, title: { type: 'string' } },
16
+ required: ['repo', 'title'],
17
+ },
18
+ },
19
+ ];
20
+
21
+ /** A fake MCP client recording calls; injected via the plugin's clientFactory. */
22
+ function fakeClient() {
23
+ const calls: Array<{ name: string; args: Record<string, unknown> }> = [];
24
+ let closed = false;
25
+ const client: McpClientLike = {
26
+ listTools: async () => TOOLS,
27
+ callTool: async (name, args) => {
28
+ calls.push({ name, args });
29
+ return { content: [{ type: 'text', text: 'created #42' }], structuredContent: { number: 42 } };
30
+ },
31
+ close: async () => { closed = true; },
32
+ };
33
+ return { client, calls, isClosed: () => closed };
34
+ }
35
+
36
+ describe('ConnectorMcpPlugin — end to end with the automation engine', () => {
37
+ it('registers the MCP-backed connector so a connector_action flow dispatches to tools/call', async () => {
38
+ const { client, calls, isClosed } = fakeClient();
39
+
40
+ const kernel = new LiteKernel();
41
+ kernel.use(new AutomationServicePlugin());
42
+ kernel.use(
43
+ new ConnectorMcpPlugin({
44
+ name: 'github_mcp',
45
+ label: 'GitHub MCP',
46
+ transport: { kind: 'stdio', command: 'noop' },
47
+ clientFactory: async () => client,
48
+ }),
49
+ );
50
+ await kernel.bootstrap();
51
+
52
+ const engine = kernel.getService<AutomationEngine>('automation');
53
+
54
+ // The baseline node and the MCP-backed connector are both present; to the
55
+ // registry it is an ordinary connector.
56
+ expect(engine.getRegisteredNodeTypes()).toContain('connector_action');
57
+ expect(engine.getRegisteredConnectors()).toContain('github_mcp');
58
+
59
+ engine.registerFlow('open_issue', {
60
+ name: 'open_issue',
61
+ label: 'Open an issue via MCP',
62
+ type: 'autolaunched',
63
+ variables: [{ name: 'call.structuredContent', type: 'json', isOutput: true }],
64
+ nodes: [
65
+ { id: 'start', type: 'start', label: 'Start' },
66
+ {
67
+ id: 'call',
68
+ type: 'connector_action',
69
+ label: 'create_issue',
70
+ connectorConfig: {
71
+ connectorId: 'github_mcp',
72
+ actionId: 'create_issue',
73
+ input: { repo: 'acme/app', title: 'Bug' },
74
+ },
75
+ },
76
+ { id: 'end', type: 'end', label: 'End' },
77
+ ],
78
+ edges: [
79
+ { id: 'e1', source: 'start', target: 'call' },
80
+ { id: 'e2', source: 'call', target: 'end' },
81
+ ],
82
+ });
83
+
84
+ const result = await engine.execute('open_issue');
85
+
86
+ expect(result.success).toBe(true);
87
+ // The MCP connector handled the dispatch: one tools/call with the input.
88
+ expect(calls).toEqual([{ name: 'create_issue', args: { repo: 'acme/app', title: 'Bug' } }]);
89
+ // The normalised structuredContent propagated back into the flow.
90
+ expect(result.output).toEqual({ 'call.structuredContent': { number: 42 } });
91
+
92
+ // Shutdown tears the MCP connection down.
93
+ await kernel.shutdown();
94
+ expect(isClosed()).toBe(true);
95
+ });
96
+ });
@@ -0,0 +1,104 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import type { Plugin, PluginContext } from '@objectstack/core';
4
+ import type { Connector } from '@objectstack/spec/integration';
5
+ import { createMcpConnector, type McpConnectorOptions } from './mcp-connector.js';
6
+
7
+ /**
8
+ * Minimal surface of the automation engine this plugin depends on — the
9
+ * connector registry from ADR-0018 §Addendum. Kept structural so the plugin
10
+ * needs no runtime dependency on `@objectstack/service-automation`.
11
+ */
12
+ export interface ConnectorRegistrySurface {
13
+ registerConnector(
14
+ def: Connector,
15
+ handlers: Record<
16
+ string,
17
+ (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>
18
+ >,
19
+ ): void;
20
+ unregisterConnector(name: string): void;
21
+ }
22
+
23
+ export interface ConnectorMcpPluginOptions extends McpConnectorOptions {}
24
+
25
+ /**
26
+ * ConnectorMcpPlugin — connects to an MCP server, discovers its tools, and
27
+ * registers them as a single connector on the automation engine (ADR-0024).
28
+ * One generic adapter, configured per server (transport + `include`), never
29
+ * per-server code.
30
+ *
31
+ * Lifecycle: on `start()` it connects and builds the connector once; on
32
+ * `stop()` it tears the MCP connection down. If no automation engine is present
33
+ * — or the server is unreachable at boot — the plugin logs and skips: a missing
34
+ * optional connector is not a fatal error (same posture as `ConnectorRestPlugin`).
35
+ */
36
+ export class ConnectorMcpPlugin implements Plugin {
37
+ name = 'com.objectstack.connector.mcp';
38
+ version = '1.0.0';
39
+ type = 'standard' as const;
40
+ // Ensure the automation engine (and its connector registry) is started first.
41
+ dependencies = ['com.objectstack.service-automation'];
42
+
43
+ private readonly options: ConnectorMcpPluginOptions;
44
+ private connectorName?: string;
45
+ private automation?: ConnectorRegistrySurface;
46
+ private close?: () => Promise<void>;
47
+
48
+ constructor(options: ConnectorMcpPluginOptions) {
49
+ this.options = options;
50
+ }
51
+
52
+ async init(_ctx: PluginContext): Promise<void> {
53
+ // No services to register; the connector is registered in start() once
54
+ // the automation engine is available and the MCP server has been queried.
55
+ }
56
+
57
+ async start(ctx: PluginContext): Promise<void> {
58
+ let automation: ConnectorRegistrySurface | undefined;
59
+ try {
60
+ automation = ctx.getService<ConnectorRegistrySurface>('automation');
61
+ } catch {
62
+ automation = undefined;
63
+ }
64
+
65
+ if (!automation || typeof automation.registerConnector !== 'function') {
66
+ ctx.logger.info('ConnectorMcpPlugin: no automation engine — MCP connector not registered');
67
+ return;
68
+ }
69
+
70
+ let bundle;
71
+ try {
72
+ bundle = await createMcpConnector(this.options);
73
+ } catch (err) {
74
+ // The MCP server is unreachable / failed discovery at boot. Skip the
75
+ // optional connector rather than failing the whole bootstrap.
76
+ ctx.logger.warn(
77
+ `ConnectorMcpPlugin: could not connect to MCP server — connector not registered: ${(err as Error).message}`,
78
+ );
79
+ return;
80
+ }
81
+
82
+ automation.registerConnector(bundle.def, bundle.handlers);
83
+ this.automation = automation;
84
+ this.connectorName = bundle.def.name;
85
+ this.close = bundle.close;
86
+ ctx.logger.info(
87
+ `ConnectorMcpPlugin: MCP connector '${bundle.def.name}' registered with ${bundle.def.actions?.length ?? 0} action(s)`,
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Destroy phase — the kernel's shutdown hook (the `Plugin` lifecycle exposes
93
+ * `destroy()`, not `stop()`). Unregister the connector and tear the MCP
94
+ * connection down so no child process / socket is leaked.
95
+ */
96
+ async destroy(): Promise<void> {
97
+ if (this.automation && this.connectorName) {
98
+ try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }
99
+ }
100
+ if (this.close) {
101
+ try { await this.close(); } catch { /* ignore */ }
102
+ }
103
+ }
104
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ /**
4
+ * @objectstack/connector-mcp
5
+ *
6
+ * A generic adapter that turns *any* Model Context Protocol (MCP) server into a
7
+ * {@link Connector} registered on the automation engine (ADR-0024). On connect
8
+ * it lists the server's tools and maps each one to a connector action; the
9
+ * baseline `connector_action` node then dispatches calls to the server's
10
+ * `tools/call`. One adapter unlocks the entire MCP ecosystem with no per-server
11
+ * code — and, because MCP is itself an LLM tool protocol, every imported tool
12
+ * doubles as an AI tool under ADR-0011.
13
+ *
14
+ * Open-source scope: the MCP client adapter (stdio + http transports),
15
+ * `tools/list` → actions, `tools/call` dispatch, and operator-supplied static
16
+ * credentials passed through the transport. A curated server registry, managed
17
+ * secrets, per-tenant lifecycle, and sandboxed stdio execution are the
18
+ * enterprise tier (ADR-0024 §4).
19
+ */
20
+
21
+ export {
22
+ createMcpConnector,
23
+ type McpConnectorOptions,
24
+ type McpConnectorBundle,
25
+ type McpTransport,
26
+ type McpToolDescriptor,
27
+ type McpClientLike,
28
+ } from './mcp-connector.js';
29
+ export {
30
+ ConnectorMcpPlugin,
31
+ type ConnectorMcpPluginOptions,
32
+ type ConnectorRegistrySurface,
33
+ } from './connector-mcp-plugin.js';