Skip to content

Commit 6e4b3f2

Browse files
os-zhuangclaude
andauthored
feat(connector-mcp): adopt MCP servers as connectors (ADR-0024) (#1424)
* feat(connector-mcp): adopt MCP servers as connectors (ADR-0024) Add @objectstack/connector-mcp — a single generic adapter that turns any Model Context Protocol server into an ordinary type:'api' connector on the automation engine, with no per-server code. - createMcpConnector(): connect over stdio or streamable-HTTP, call tools/list, map each tool to a connector action (name→key, description→label/description, inputSchema→inputSchema), and dispatch connector_action calls to tools/call. Results normalise to the shared { ok, content, ... } envelope; logical tool errors surface as ok:false. - ConnectorMcpPlugin: registers via the existing engine.registerConnector() path (no new engine surface, no mcp_call node) and tears the MCP connection down on destroy(). Fail-soft when the server or automation engine is absent. - Credentials stay with the MCP server (transport env/headers), never in ConnectorSchema or the discovery-exposed def. - Mirrors the connector-rest/connector-slack conventions; the real MCP SDK client is lazily imported and injectable so unit tests stay hermetic. https://claude.ai/code/session_01G9v2WA48voVBSkQ7sjFkqC * chore(connector-mcp): add to Changesets fixed group Keeps @objectstack/connector-mcp versioned in lockstep with the rest of the public monorepo, satisfying scripts/check-changeset-fixed.mjs (the Validate Package Dependencies CI gate). https://claude.ai/code/session_01G9v2WA48voVBSkQ7sjFkqC --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 75a0bca commit 6e4b3f2

11 files changed

Lines changed: 886 additions & 0 deletions

File tree

.changeset/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@objectstack/plugin-sharing",
3939
"@objectstack/plugin-webhooks",
4040
"@objectstack/plugin-trigger-record-change",
41+
"@objectstack/connector-mcp",
4142
"@objectstack/connector-rest",
4243
"@objectstack/connector-slack",
4344
"@objectstack/express",

.changeset/connector-mcp.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/connector-mcp": minor
3+
---
4+
5+
MCP Connector (ADR-0024) — adopt any Model Context Protocol server as a connector.
6+
7+
Adds `@objectstack/connector-mcp`, a single generic adapter that turns *any*
8+
MCP server into an ordinary `type: 'api'` connector on the automation engine —
9+
no per-server code:
10+
11+
- `createMcpConnector({ transport, include?, … })` connects over **stdio** or
12+
**streamable-HTTP**, calls `tools/list`, and maps each tool to a connector
13+
action (`name → key`, `description → label/description`,
14+
`inputSchema → inputSchema`). Handlers dispatch to `tools/call` and normalise
15+
the result to the shared `{ ok, content, … }` envelope (logical tool errors
16+
surface as `ok: false` rather than throwing).
17+
- `ConnectorMcpPlugin` registers the connector via the existing
18+
`engine.registerConnector()` path (no new engine surface, no `mcp_call` node)
19+
and tears the MCP connection down on shutdown. Fail-soft: an unreachable
20+
server or missing automation engine is logged and skipped.
21+
- Credentials live with the MCP server (transport `env`/`headers`), never in
22+
`ConnectorSchema` and never in the serialized, discovery-exposed `def`.
23+
24+
Open-tier scope: client adapter + operator-supplied static credentials. A
25+
curated server registry, managed secrets, per-tenant lifecycle, and sandboxed
26+
stdio execution remain the enterprise tier.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved.
2+
"Business Source License" is a trademark of MariaDB Corporation Ab.
3+
4+
Parameters
5+
6+
Licensor: ObjectStack AI LLC
7+
Licensed Work: ObjectStack Runtime: the BSL-licensed packages
8+
of the ObjectStack monorepo as listed in LICENSING.md.
9+
Copyright (c) 2026 ObjectStack AI LLC.
10+
Additional Use Grant: You may make production use of the Licensed Work, provided
11+
Your use does not include offering the Licensed Work to third
12+
parties on a hosted or embedded basis in order to compete with
13+
ObjectStack AI LLC's paid version(s) of the Licensed Work. For purposes
14+
of this license:
15+
16+
A "competitive offering" is a Product that is offered to third
17+
parties on a paid basis, including through paid support
18+
arrangements, that significantly overlaps with the capabilities
19+
of ObjectStack AI LLC's paid version(s) of the Licensed Work. If Your
20+
Product is not a competitive offering when You first make it
21+
generally available, it will not become a competitive offering
22+
later due to ObjectStack AI LLC releasing a new version of the Licensed
23+
Work with additional capabilities. In addition, Products that
24+
are not provided on a paid basis are not competitive.
25+
26+
"Product" means software that is offered to end users to manage
27+
in their own environments or offered as a service on a hosted
28+
basis.
29+
30+
"Embedded" means including the source code or executable code
31+
from the Licensed Work in a competitive offering. "Embedded"
32+
also means packaging the competitive offering in such a way
33+
that the Licensed Work must be accessed or downloaded for the
34+
competitive offering to operate.
35+
36+
Hosting or using the Licensed Work(s) for internal purposes
37+
within an organization is not considered a competitive
38+
offering. ObjectStack AI LLC considers your organization to include all
39+
of your affiliates under common control.
40+
41+
For binding interpretive guidance on using ObjectStack AI LLC products
42+
under the Business Source License, please visit our FAQ.
43+
(see LICENSING.md in this repository)
44+
Change Date: Four years from the date the Licensed Work is published.
45+
Change License: Apache License, Version 2.0
46+
47+
For information about alternative licensing arrangements for the Licensed Work,
48+
please contact licensing@objectstack.dev.
49+
50+
Notice
51+
52+
Business Source License 1.1
53+
54+
Terms
55+
56+
The Licensor hereby grants you the right to copy, modify, create derivative
57+
works, redistribute, and make non-production use of the Licensed Work. The
58+
Licensor may make an Additional Use Grant, above, permitting limited production use.
59+
60+
Effective on the Change Date, or the fourth anniversary of the first publicly
61+
available distribution of a specific version of the Licensed Work under this
62+
License, whichever comes first, the Licensor hereby grants you rights under
63+
the terms of the Change License, and the rights granted in the paragraph
64+
above terminate.
65+
66+
If your use of the Licensed Work does not comply with the requirements
67+
currently in effect as described in this License, you must purchase a
68+
commercial license from the Licensor, its affiliated entities, or authorized
69+
resellers, or you must refrain from using the Licensed Work.
70+
71+
All copies of the original and modified Licensed Work, and derivative works
72+
of the Licensed Work, are subject to this License. This License applies
73+
separately for each version of the Licensed Work and the Change Date may vary
74+
for each version of the Licensed Work released by Licensor.
75+
76+
You must conspicuously display this License on each original or modified copy
77+
of the Licensed Work. If you receive the Licensed Work in original or
78+
modified form from a third party, the terms and conditions set forth in this
79+
License apply to your use of that work.
80+
81+
Any use of the Licensed Work in violation of this License will automatically
82+
terminate your rights under this License for the current and all other
83+
versions of the Licensed Work.
84+
85+
This License does not grant you any right in any trademark or logo of
86+
Licensor or its affiliates (provided that you may use a trademark or logo of
87+
Licensor as expressly required by this License).
88+
89+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
90+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
91+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
92+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
93+
TITLE.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "@objectstack/connector-mcp",
3+
"version": "7.3.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+
"scripts": {
16+
"build": "tsup --config ../../../tsup.config.ts",
17+
"test": "vitest run --passWithNoTests"
18+
},
19+
"dependencies": {
20+
"@modelcontextprotocol/sdk": "^1.29.0",
21+
"@objectstack/core": "workspace:*",
22+
"@objectstack/spec": "workspace:*"
23+
},
24+
"devDependencies": {
25+
"@objectstack/service-automation": "workspace:*",
26+
"@types/node": "^25.9.1",
27+
"typescript": "^6.0.3",
28+
"vitest": "^4.1.7"
29+
},
30+
"keywords": [
31+
"objectstack",
32+
"connector",
33+
"mcp",
34+
"model-context-protocol",
35+
"integration",
36+
"ai",
37+
"tools"
38+
]
39+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)