@objectstack/connector-mcp 17.0.0-rc.0 → 17.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +210 -0
- package/package.json +11 -5
- package/.turbo/turbo-build.log +0 -22
- package/src/connector-mcp-plugin.test.ts +0 -127
- package/src/connector-mcp-plugin.ts +0 -149
- package/src/index.ts +0 -39
- package/src/mcp-connector.test.ts +0 -194
- package/src/mcp-connector.ts +0 -272
- package/src/mcp-provider.test.ts +0 -193
- package/src/mcp-provider.ts +0 -205
- package/tsconfig.json +0 -10
package/src/mcp-provider.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectstack/spec/integration';
|
|
4
|
-
import { ConnectorUpstreamUnavailableError } from '@objectstack/spec/integration';
|
|
5
|
-
import { createMcpConnector, type McpConnectorOptions, type McpTransport } from './mcp-connector.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The provider key this package contributes (ADR-0097). A declarative
|
|
9
|
-
* `connectors:` entry with `provider: 'mcp'` is materialized by this factory.
|
|
10
|
-
*/
|
|
11
|
-
export const MCP_PROVIDER_KEY = 'mcp';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Host policy for **declarative** stdio transports (#3055). A stdio transport
|
|
15
|
-
* launches a local child process, and declarative entries arrive through
|
|
16
|
-
* metadata — including a runtime Studio publish — so spawning from them is
|
|
17
|
-
* gated OFF by default:
|
|
18
|
-
*
|
|
19
|
-
* - `undefined` / `false` — deny (default): a `provider: 'mcp'` entry with a
|
|
20
|
-
* stdio transport is rejected as a configuration fault.
|
|
21
|
-
* - `string[]` — allowlist: the transport's `command` must strictly equal one
|
|
22
|
-
* of the listed commands. NOTE this is a coarse trust boundary — listing a
|
|
23
|
-
* launcher like `npx` effectively allows any package it can run; list the
|
|
24
|
-
* specific server binaries you trust. Sandboxed execution is the enterprise
|
|
25
|
-
* tier (ADR-0024 §4).
|
|
26
|
-
* - `true` — allow any command (explicit full trust; hosts that treat every
|
|
27
|
-
* metadata author as an operator).
|
|
28
|
-
*
|
|
29
|
-
* Hand-wired connectors (plugin instance options / `createMcpConnector`) are
|
|
30
|
-
* NOT subject to this policy: their command was written in host code, a
|
|
31
|
-
* different trust anchor than metadata.
|
|
32
|
-
*/
|
|
33
|
-
export type McpDeclarativeStdioPolicy = boolean | string[];
|
|
34
|
-
|
|
35
|
-
/** Injectable dependencies for {@link createMcpProviderFactory} (tests). */
|
|
36
|
-
export interface McpProviderDeps {
|
|
37
|
-
/** Injected MCP client factory; defaults to the SDK-backed client. */
|
|
38
|
-
clientFactory?: McpConnectorOptions['clientFactory'];
|
|
39
|
-
/** Policy for declarative stdio transports (#3055). Default: deny. */
|
|
40
|
-
declarativeStdio?: McpDeclarativeStdioPolicy;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Shape of `providerConfig` for a `provider: 'mcp'` declarative instance. */
|
|
44
|
-
interface McpProviderConfig {
|
|
45
|
-
/** How to reach the MCP server (stdio or streamable-http). */
|
|
46
|
-
transport?: unknown;
|
|
47
|
-
/** Optional tool-name allowlist — only these tools become actions. */
|
|
48
|
-
include?: unknown;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isStringRecord(v: unknown): v is Record<string, string> {
|
|
52
|
-
if (!v || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
53
|
-
return Object.values(v as Record<string, unknown>).every((x) => typeof x === 'string');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Fold the resolved instance `auth` into an MCP **http** transport's headers
|
|
58
|
-
* (ADR-0024 keeps MCP credentials with the transport). `credentialRef` has
|
|
59
|
-
* already been resolved upstream, so this only maps the static credential to the
|
|
60
|
-
* right header. Not applied to stdio transports — a stdio server receives its
|
|
61
|
-
* credentials through `transport.env`.
|
|
62
|
-
*/
|
|
63
|
-
function applyAuthToHeaders(
|
|
64
|
-
auth: ResolvedConnectorAuth | undefined,
|
|
65
|
-
headers: Record<string, string>,
|
|
66
|
-
): void {
|
|
67
|
-
if (!auth || auth.type === 'none') return;
|
|
68
|
-
switch (auth.type) {
|
|
69
|
-
case 'bearer':
|
|
70
|
-
headers['Authorization'] = `Bearer ${auth.token}`;
|
|
71
|
-
return;
|
|
72
|
-
case 'basic':
|
|
73
|
-
headers['Authorization'] = `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`;
|
|
74
|
-
return;
|
|
75
|
-
case 'api-key':
|
|
76
|
-
// Header-based only for MCP http (query-param keys are not part of the transport).
|
|
77
|
-
if (!auth.paramName) headers[auth.headerName ?? 'X-API-Key'] = auth.key;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Validate + normalize `providerConfig.transport`, injecting resolved auth for http. */
|
|
83
|
-
function normalizeTransport(
|
|
84
|
-
raw: unknown,
|
|
85
|
-
connectorName: string,
|
|
86
|
-
auth: ResolvedConnectorAuth | undefined,
|
|
87
|
-
): McpTransport {
|
|
88
|
-
if (!raw || typeof raw !== 'object') {
|
|
89
|
-
throw new Error(
|
|
90
|
-
`connector-mcp provider: connector '${connectorName}' requires providerConfig.transport ` +
|
|
91
|
-
`({ kind: 'stdio', command, ... } or { kind: 'http', url, ... }).`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
const t = raw as Record<string, unknown>;
|
|
95
|
-
if (t.kind === 'stdio') {
|
|
96
|
-
if (typeof t.command !== 'string' || t.command.length === 0) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`connector-mcp provider: connector '${connectorName}' stdio transport requires a 'command' string.`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
kind: 'stdio',
|
|
103
|
-
command: t.command,
|
|
104
|
-
args: Array.isArray(t.args) ? t.args.map((a) => String(a)) : undefined,
|
|
105
|
-
env: isStringRecord(t.env) ? t.env : undefined,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
if (t.kind === 'http') {
|
|
109
|
-
if (typeof t.url !== 'string' || t.url.length === 0) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`connector-mcp provider: connector '${connectorName}' http transport requires a 'url' string.`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
const headers: Record<string, string> = { ...(isStringRecord(t.headers) ? t.headers : {}) };
|
|
115
|
-
applyAuthToHeaders(auth, headers);
|
|
116
|
-
return { kind: 'http', url: t.url, headers: Object.keys(headers).length > 0 ? headers : undefined };
|
|
117
|
-
}
|
|
118
|
-
throw new Error(
|
|
119
|
-
`connector-mcp provider: connector '${connectorName}' providerConfig.transport.kind must be 'stdio' or 'http'.`,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Enforce the {@link McpDeclarativeStdioPolicy} for one declarative instance
|
|
125
|
-
* (#3055). Throws a **plain** Error on violation: a security-policy rejection
|
|
126
|
-
* is a configuration fault — fatal at boot, skipped+logged on reload — and must
|
|
127
|
-
* never be classified upstream-unavailable (it cannot be retried into
|
|
128
|
-
* existence).
|
|
129
|
-
*/
|
|
130
|
-
function assertDeclarativeStdioAllowed(
|
|
131
|
-
policy: McpDeclarativeStdioPolicy | undefined,
|
|
132
|
-
command: string,
|
|
133
|
-
connectorName: string,
|
|
134
|
-
): void {
|
|
135
|
-
if (policy === true) return;
|
|
136
|
-
if (Array.isArray(policy)) {
|
|
137
|
-
if (policy.includes(command)) return;
|
|
138
|
-
throw new Error(
|
|
139
|
-
`connector-mcp provider: connector '${connectorName}' declares a stdio transport with command '${command}', ` +
|
|
140
|
-
`which is not in the host's declarativeStdio allowlist [${policy.join(', ')}]. ` +
|
|
141
|
-
`Add the command to new ConnectorMcpPlugin({ declarativeStdio: [...] }) if this server is trusted (#3055).`,
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
throw new Error(
|
|
145
|
-
`connector-mcp provider: connector '${connectorName}' declares a stdio transport (command '${command}'), ` +
|
|
146
|
-
`but declarative stdio transports are disabled by default — a stdio transport launches a local process ` +
|
|
147
|
-
`from stack metadata (including runtime Studio publishes). If this server is trusted, opt in deliberately: ` +
|
|
148
|
-
`new ConnectorMcpPlugin({ declarativeStdio: ['${command}'] }) — or use an http transport (#3055, ADR-0024 §4).`,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Build the `mcp` {@link ConnectorProviderFactory} (ADR-0097 / ADR-0024). At boot
|
|
154
|
-
* the automation service invokes it for each `provider: 'mcp'` declarative
|
|
155
|
-
* instance: it connects to the MCP server named by `providerConfig.transport`,
|
|
156
|
-
* lists its tools, and produces the same `{ def, handlers, close }` bundle
|
|
157
|
-
* {@link createMcpConnector} builds for a hand-wired MCP connector — one action
|
|
158
|
-
* per tool, dispatched to the server's `tools/call`.
|
|
159
|
-
*
|
|
160
|
-
* Stdio transports on declarative instances are policy-gated (default deny) —
|
|
161
|
-
* see {@link McpDeclarativeStdioPolicy} (#3055).
|
|
162
|
-
*
|
|
163
|
-
* The connection is opened at materialization. Faults are classified (#3017):
|
|
164
|
-
* an invalid transport shape is a *configuration* fault and throws plain —
|
|
165
|
-
* fatal at boot per the ADR-0097 fail-loud contract — while a connect /
|
|
166
|
-
* `tools/list` failure (server down, refused, timed out) is an *operational*
|
|
167
|
-
* fault and throws {@link ConnectorUpstreamUnavailableError}, which the
|
|
168
|
-
* materializer turns into a degraded instance that is retried with backoff
|
|
169
|
-
* instead of aborting the whole app boot.
|
|
170
|
-
*/
|
|
171
|
-
export function createMcpProviderFactory(deps: McpProviderDeps = {}): ConnectorProviderFactory {
|
|
172
|
-
return async (ctx) => {
|
|
173
|
-
const cfg = (ctx.providerConfig ?? {}) as McpProviderConfig;
|
|
174
|
-
const transport = normalizeTransport(cfg.transport, ctx.name, ctx.auth);
|
|
175
|
-
if (transport.kind === 'stdio') {
|
|
176
|
-
assertDeclarativeStdioAllowed(deps.declarativeStdio, transport.command, ctx.name);
|
|
177
|
-
}
|
|
178
|
-
const includeList = Array.isArray(cfg.include)
|
|
179
|
-
? cfg.include.filter((x): x is string => typeof x === 'string')
|
|
180
|
-
: undefined;
|
|
181
|
-
const include = includeList ? (toolName: string) => includeList.includes(toolName) : undefined;
|
|
182
|
-
|
|
183
|
-
let bundle;
|
|
184
|
-
try {
|
|
185
|
-
bundle = await createMcpConnector({
|
|
186
|
-
name: ctx.name,
|
|
187
|
-
label: ctx.label,
|
|
188
|
-
description: ctx.description,
|
|
189
|
-
transport,
|
|
190
|
-
include,
|
|
191
|
-
clientFactory: deps.clientFactory,
|
|
192
|
-
});
|
|
193
|
-
} catch (err) {
|
|
194
|
-
// Everything past transport validation is talking to the server (connect,
|
|
195
|
-
// handshake, tools/list) — operational, hence retryable. A credential the
|
|
196
|
-
// server rejects also lands here: indistinguishable from the outside, and
|
|
197
|
-
// retrying it is loud (logged per attempt), never silent.
|
|
198
|
-
throw new ConnectorUpstreamUnavailableError(
|
|
199
|
-
`connector-mcp provider: connector '${ctx.name}' could not reach its MCP server: ${(err as Error).message}`,
|
|
200
|
-
{ cause: err },
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
return { def: bundle.def, handlers: bundle.handlers, close: bundle.close };
|
|
204
|
-
};
|
|
205
|
-
}
|