@objectstack/connector-mcp 16.1.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 +336 -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.test.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
//
|
|
3
|
-
// ADR-0097 — the `mcp` provider factory: materialize a declarative
|
|
4
|
-
// `provider: 'mcp'` connector instance by connecting to the server (an injected
|
|
5
|
-
// fake client here), listing its tools, and mapping them to actions.
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect } from 'vitest';
|
|
8
|
-
import type { ConnectorProviderContext } from '@objectstack/spec/integration';
|
|
9
|
-
import { isConnectorUpstreamUnavailable } from '@objectstack/spec/integration';
|
|
10
|
-
import type { McpClientLike, McpToolDescriptor, McpTransport } from './mcp-connector.js';
|
|
11
|
-
import { createMcpProviderFactory, MCP_PROVIDER_KEY } from './mcp-provider.js';
|
|
12
|
-
|
|
13
|
-
const TOOLS: McpToolDescriptor[] = [
|
|
14
|
-
{ name: 'create_issue', description: 'Create an issue', inputSchema: { type: 'object' } },
|
|
15
|
-
{ name: 'list_issues', description: 'List issues' },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
/** Capture the transport the factory built, and serve fixed tools. */
|
|
19
|
-
function fakeClientFactory() {
|
|
20
|
-
const seen: { transport?: McpTransport } = {};
|
|
21
|
-
let closed = false;
|
|
22
|
-
const factory = async (transport: McpTransport): Promise<McpClientLike> => {
|
|
23
|
-
seen.transport = transport;
|
|
24
|
-
return {
|
|
25
|
-
listTools: async () => TOOLS,
|
|
26
|
-
callTool: async () => ({ content: [{ type: 'text', text: 'ok' }] }),
|
|
27
|
-
close: async () => { closed = true; },
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
return { factory, seen, isClosed: () => closed };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function ctx(partial: Partial<ConnectorProviderContext> & Pick<ConnectorProviderContext, 'providerConfig'>): ConnectorProviderContext {
|
|
34
|
-
return { name: 'github', label: 'GitHub', type: 'api', ...partial };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('mcp provider factory (ADR-0097)', () => {
|
|
38
|
-
it('advertises the mcp provider key', () => {
|
|
39
|
-
expect(MCP_PROVIDER_KEY).toBe('mcp');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('connects, lists tools, and maps them to actions', async () => {
|
|
43
|
-
const { factory: clientFactory } = fakeClientFactory();
|
|
44
|
-
// stdio on a declarative instance requires the host opt-in (#3055).
|
|
45
|
-
const factory = createMcpProviderFactory({ clientFactory, declarativeStdio: ['my-mcp'] });
|
|
46
|
-
const mat = await factory(ctx({ providerConfig: { transport: { kind: 'stdio', command: 'my-mcp' } } }));
|
|
47
|
-
expect(mat.def.name).toBe('github');
|
|
48
|
-
expect(Object.keys(mat.handlers).sort()).toEqual(['create_issue', 'list_issues']);
|
|
49
|
-
expect(typeof mat.close).toBe('function');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('applies the tool allowlist from providerConfig.include', async () => {
|
|
53
|
-
const { factory: clientFactory } = fakeClientFactory();
|
|
54
|
-
const factory = createMcpProviderFactory({ clientFactory, declarativeStdio: ['my-mcp'] });
|
|
55
|
-
const mat = await factory(
|
|
56
|
-
ctx({ providerConfig: { transport: { kind: 'stdio', command: 'my-mcp' }, include: ['create_issue'] } }),
|
|
57
|
-
);
|
|
58
|
-
expect(Object.keys(mat.handlers)).toEqual(['create_issue']);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('folds resolved bearer auth into an http transport header', async () => {
|
|
62
|
-
const captured = fakeClientFactory();
|
|
63
|
-
const factory = createMcpProviderFactory({ clientFactory: captured.factory });
|
|
64
|
-
await factory(
|
|
65
|
-
ctx({
|
|
66
|
-
providerConfig: { transport: { kind: 'http', url: 'https://mcp.example.com' } },
|
|
67
|
-
auth: { type: 'bearer', token: 'tok' },
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
const t = captured.seen.transport;
|
|
71
|
-
expect(t?.kind).toBe('http');
|
|
72
|
-
expect(t?.kind === 'http' && t.headers?.Authorization).toBe('Bearer tok');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('throws when the transport is missing', async () => {
|
|
76
|
-
const factory = createMcpProviderFactory();
|
|
77
|
-
await expect(factory(ctx({ providerConfig: {} }))).rejects.toThrow(/providerConfig\.transport/);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('throws for an unknown transport kind', async () => {
|
|
81
|
-
const factory = createMcpProviderFactory();
|
|
82
|
-
await expect(
|
|
83
|
-
factory(ctx({ providerConfig: { transport: { kind: 'carrier-pigeon' } } })),
|
|
84
|
-
).rejects.toThrow(/kind must be 'stdio' or 'http'/);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// ── #3017 — fault classification: config stays fatal, upstream degrades ─────
|
|
89
|
-
|
|
90
|
-
describe('mcp provider fault classification (#3017)', () => {
|
|
91
|
-
const stdio = { transport: { kind: 'stdio', command: 'my-mcp' } };
|
|
92
|
-
const allowMyMcp = { declarativeStdio: ['my-mcp'] };
|
|
93
|
-
|
|
94
|
-
it('classifies a connect failure as upstream-unavailable (retryable), keeping the cause', async () => {
|
|
95
|
-
const boom = new Error('connect ECONNREFUSED 127.0.0.1:9999');
|
|
96
|
-
const clientFactory = async (): Promise<McpClientLike> => { throw boom; };
|
|
97
|
-
const factory = createMcpProviderFactory({ clientFactory, ...allowMyMcp });
|
|
98
|
-
|
|
99
|
-
const err = await factory(ctx({ providerConfig: stdio })).then(
|
|
100
|
-
() => { throw new Error('expected rejection'); },
|
|
101
|
-
(e: unknown) => e,
|
|
102
|
-
);
|
|
103
|
-
expect(isConnectorUpstreamUnavailable(err)).toBe(true);
|
|
104
|
-
expect((err as Error).message).toMatch(/'github' could not reach its MCP server/);
|
|
105
|
-
expect((err as Error).message).toContain('ECONNREFUSED');
|
|
106
|
-
expect((err as { cause?: unknown }).cause).toBe(boom);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('classifies a tools/list failure as upstream-unavailable and closes the client', async () => {
|
|
110
|
-
let closed = false;
|
|
111
|
-
const clientFactory = async (): Promise<McpClientLike> => ({
|
|
112
|
-
listTools: async () => { throw new Error('request timed out'); },
|
|
113
|
-
callTool: async () => ({}),
|
|
114
|
-
close: async () => { closed = true; },
|
|
115
|
-
});
|
|
116
|
-
const factory = createMcpProviderFactory({ clientFactory, ...allowMyMcp });
|
|
117
|
-
|
|
118
|
-
const err = await factory(ctx({ providerConfig: stdio })).then(
|
|
119
|
-
() => { throw new Error('expected rejection'); },
|
|
120
|
-
(e: unknown) => e,
|
|
121
|
-
);
|
|
122
|
-
expect(isConnectorUpstreamUnavailable(err)).toBe(true);
|
|
123
|
-
expect(closed).toBe(true); // discovery failure must not leak the connection
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('keeps transport-shape faults plain — configuration errors stay fatal at boot', async () => {
|
|
127
|
-
const factory = createMcpProviderFactory();
|
|
128
|
-
const err = await factory(ctx({ providerConfig: {} })).then(
|
|
129
|
-
() => { throw new Error('expected rejection'); },
|
|
130
|
-
(e: unknown) => e,
|
|
131
|
-
);
|
|
132
|
-
expect(isConnectorUpstreamUnavailable(err)).toBe(false);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// ── #3055 — declarative stdio policy: default-deny + host allowlist ─────────
|
|
137
|
-
//
|
|
138
|
-
// A declarative stdio transport spawns a local process from metadata (a Studio
|
|
139
|
-
// publish reaches materialization at runtime), so it is gated OFF unless the
|
|
140
|
-
// host opts in. Violations are CONFIGURATION faults: plain throw (fatal at
|
|
141
|
-
// boot, skipped on reload) — never upstream-unavailable, which would retry a
|
|
142
|
-
// security rejection into existence.
|
|
143
|
-
|
|
144
|
-
describe('mcp provider declarative stdio policy (#3055)', () => {
|
|
145
|
-
const stdioCfg = { transport: { kind: 'stdio', command: 'my-mcp' } };
|
|
146
|
-
|
|
147
|
-
it('DENIES a declarative stdio transport by default, as a plain (non-retryable) fault', async () => {
|
|
148
|
-
const { factory: clientFactory, seen } = fakeClientFactory();
|
|
149
|
-
const factory = createMcpProviderFactory({ clientFactory }); // no policy
|
|
150
|
-
const err = await factory(ctx({ providerConfig: stdioCfg })).then(
|
|
151
|
-
() => { throw new Error('expected rejection'); },
|
|
152
|
-
(e: unknown) => e,
|
|
153
|
-
);
|
|
154
|
-
expect((err as Error).message).toMatch(/stdio transports are disabled by default/);
|
|
155
|
-
expect((err as Error).message).toContain("declarativeStdio: ['my-mcp']"); // actionable opt-in hint
|
|
156
|
-
expect(isConnectorUpstreamUnavailable(err)).toBe(false);
|
|
157
|
-
expect(seen.transport).toBeUndefined(); // rejected before any connection attempt
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('allowlist admits exactly the listed command and rejects others', async () => {
|
|
161
|
-
const { factory: clientFactory } = fakeClientFactory();
|
|
162
|
-
const factory = createMcpProviderFactory({ clientFactory, declarativeStdio: ['npx', 'my-mcp'] });
|
|
163
|
-
const mat = await factory(ctx({ providerConfig: stdioCfg }));
|
|
164
|
-
expect(mat.def.name).toBe('github');
|
|
165
|
-
|
|
166
|
-
const err = await factory(
|
|
167
|
-
ctx({ providerConfig: { transport: { kind: 'stdio', command: 'bash' } } }),
|
|
168
|
-
).then(
|
|
169
|
-
() => { throw new Error('expected rejection'); },
|
|
170
|
-
(e: unknown) => e,
|
|
171
|
-
);
|
|
172
|
-
expect((err as Error).message).toMatch(/not in the host's declarativeStdio allowlist \[npx, my-mcp\]/);
|
|
173
|
-
expect(isConnectorUpstreamUnavailable(err)).toBe(false);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('declarativeStdio: true allows any command (explicit full trust)', async () => {
|
|
177
|
-
const { factory: clientFactory } = fakeClientFactory();
|
|
178
|
-
const factory = createMcpProviderFactory({ clientFactory, declarativeStdio: true });
|
|
179
|
-
const mat = await factory(
|
|
180
|
-
ctx({ providerConfig: { transport: { kind: 'stdio', command: 'anything' } } }),
|
|
181
|
-
);
|
|
182
|
-
expect(Object.keys(mat.handlers).length).toBeGreaterThan(0);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('http transports are NOT subject to the policy', async () => {
|
|
186
|
-
const { factory: clientFactory } = fakeClientFactory();
|
|
187
|
-
const factory = createMcpProviderFactory({ clientFactory }); // default-deny policy in force
|
|
188
|
-
const mat = await factory(
|
|
189
|
-
ctx({ providerConfig: { transport: { kind: 'http', url: 'https://mcp.example.com' } } }),
|
|
190
|
-
);
|
|
191
|
-
expect(mat.def.name).toBe('github');
|
|
192
|
-
});
|
|
193
|
-
});
|
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
|
-
}
|