Skip to content

Commit 4694365

Browse files
fix(connector-mcp)!: refuse an unreadable or unknown providerConfig.include instead of exposing every tool (#27985)
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F <!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuPMZ8BHWtPf6nBTp51qSM64)_ Fixes #27402 Clause-②: no (narrowing) Metadata-protocol: narrowed Skills-docs: updated here — `content/docs/automation/connectors.mdx` (the `provider: 'mcp'` section now says what the `openapi` section already says: a shape other than an array of tool names, or an entry the server does not list, fails the boot and never falls back to every tool; added to the file surface by claim amendment 6515646431). The skill's MCP recipe (`skills/objectstack-automation/references/integration-recipes.md` §2) already writes `include: ['search_issues']`, which stays valid; nothing under `skills/**` is touched. ## 维护者速读 - **改了什么**:声明式 `provider: 'mcp'` 连接器实例的 `providerConfig.include`(工具白名单)现在会响亮拒收两种错误写法:不是"非空字符串数组"的形状(如字符串 `'web_search'`)在连接前拒收;点名了 MCP 服务端 `tools/list` 里没有的工具,在列出后拒收(先关连接)。两者都是配置错误:启动时失败,重载时跳过该条目。 - **为什么改**:以前把字符串写成 `include: 'web_search'` 会被当成"没有白名单",实例把服务端的全部工具(包括 `delete_everything` 这类破坏性工具)暴露给流程和智能体,且没有任何报错;写错工具名也被静默忽略。这是白名单失效放行(p1),并让 `include` 与 `openapi` provider(#27387)一个含义。 - **风险与回滚**:收窄(BREAKING):已有应用若 `include` 写成非数组或列了服务端没有的工具,升级后启动会失败并在报错里点名条目与服务端工具清单;改成工具名数组或删掉错的条目即可。回滚 = revert 本 PR。 - **先看**:`packages/connectors/connector-mcp/src/mcp-provider.ts`(`createMcpProviderFactory` 里的形状拒收与未知条目拒收两段);`mcp-provider.test.ts` 末尾 `#27402` 一组;`.changeset/27402-mcp-include-refuses.md`。 - **两分钟验证**:`pnpm --filter @objectstack/connector-mcp exec vitest run src/mcp-provider.test.ts`,应见 34/34 通过,其中 `providerConfig.include refuses loudly (#27402)` 一组 10 条全绿。 - **需要你做的**:无,席位落地(收窄:落地前由席位做契约复核)。 ## What changes `createMcpProviderFactory` (the factory the boot materializer calls for every `provider: 'mcp'` instance) now validates `providerConfig.include` with the `openapi` provider's meaning (#26709 triage 6487372008, #27387): - **Shape, before connecting.** `include` absent ⇒ every tool, as before. Otherwise it must be an array of non-empty strings; anything else (a string, an object, `null`, a non-string or empty entry) throws a plain `Error`: `connector-mcp provider: connector 'NAME' providerConfig.include must be an array of tool names (strings): each the name of a tool the MCP server lists in tools/list.` No client is created. - **Unknown entries, after `tools/list`.** The allowlist rides `createMcpConnector`'s own `include` predicate, which records every tool name the server lists. An entry nothing matched closes the connection and throws a plain `Error` naming the entries and up to 20 of the server's tools: `... providerConfig.include names 'nope', which the MCP server does not list. Each entry must be the name of a tool from the server's tools/list; the server lists 2 tool(s): 'web_search', 'delete_everything'.` (same shape and sample size as the `openapi` refusal). - Both are configuration faults on the ADR-0097 split (neither `ConnectorUpstreamUnavailableError` nor `ConnectorCredentialRejectedError`): fatal at boot, a skipped entry on reload. The instance never falls back to exposing every tool. Nothing outside `createMcpProviderFactory` moves (its JSDoc gains one paragraph). `McpProviderConfig.include` keeps its `unknown` type; `createMcpConnector` (hand-wired, host-code predicate) is unchanged. ## Tests `packages/connectors/connector-mcp/src/mcp-provider.test.ts`, new group `mcp provider: providerConfig.include refuses loudly (#27402)`, against a fake server listing `web_search` + `delete_everything` (the card's measurement): - the card's three shapes: `include: 'web_search'`, `['web_search', 7]`, `['nope']`; plus `['web_search', '']`, an object, `null`, and a partial `['web_search', 'nope', 'nope']` (no partial instance). Each asserts a configuration fault (not upstream-unavailable, not credential-rejected), the message's first sentence, and that the shape refusals never connect / the unknown-entry refusal closes the client. - an include of listed tools exposes exactly those; **control:** no `include` exposes every tool. - through the real door: a `LiteKernel` with `AutomationServicePlugin` + `ConnectorMcpPlugin` and a declared instance with `include: 'web_search'` fails `bootstrap()` with `failed to materialize connector instance 'vendor_mcp' via provider 'mcp'` + the include message, never connects, and registers no action under the name. <details><summary>Red first (fix stashed, base ed0e71d): 8 failed / 26 passed</summary> ``` × refuses an include that is a string, before connecting Error: expected a refusal, got actions web_search, delete_everything × refuses an include that is an array with a non-string entry Error: expected a refusal, got actions web_search × refuses an include that is an array with an empty name × refuses an include that is an object, before connecting × refuses an include that is null, before connecting × refuses an entry naming a tool the server does not list ... Error: expected a refusal, got actions × refuses when one entry is listed and another is not: no partial instance × a declarative instance with a string include fails boot ... AssertionError: expected undefined to be an instance of Error Tests 8 failed | 26 passed (34) ``` </details> <details><summary>Green and ablation (scripts/ablation-replace.mjs, each leg restored blob == HEAD ae66f3d, git diff HEAD empty)</summary> - green: `vitest run src/mcp-provider.test.ts` 34/34; `pnpm --filter @objectstack/connector-mcp test` 5 files / 56 tests passed; `typecheck` exit 0 (test layer: 1 pinned debt signature held, unchanged); `eslint src` exit 0. - ablation 1, shape check off (`false && cfg.include !== undefined &&`): 6 failed (the five shape cases + the kernel boot case). - ablation 2, unknown-entry check off (`if (false && unknown.length > 0)`): 2 failed (the unknown and the partial case). - ablation 3, `await bundle.close()` deleted: 1 failed (the unknown-entry case's `closed` assertion). - The test imports `./mcp-provider.js` from source (no `dist` hop), so no rebuild leg applies. </details> ## Acceptance notes - "Rejected with the reason" (triage 6489254710) lands as the materializer's existing configuration-fault path, the same as `openapi`: the boot fails naming the instance and the fault; on a reload the entry is skipped and logged, and an instance already serving keeps its previous (valid) bundle. No new instance state is added (that would be outside the file surface). - `include: []` stays accepted and exposes no tools, as for `openapi`. - Out of scope, not touched (skills lane, seat files it): `skills/objectstack-automation/references/integration-recipes.md:63` and `skills/objectstack-automation/evals/integrations-declared.json:7` still say the `openapi` provider has no `include` key, which #27387 made false. - `git merge-tree` against every open PR touching `packages/connectors/connector-mcp/**` or `content/docs/automation/connectors.mdx`: only #26517 (CHANGELOG/package.json), clean. ## Verification **Dogfood battery at the final head `6840f48c`** (after merging `origin/main` once; closure built under `os-verify-lock`, then `pnpm --filter @objectstack/dogfood test`, no shard, no path): `os-verify-lock: VERDICT command-exit 0` · Test Files 357 passed | 3 skipped (360) · Tests 2875 passed | 20 skipped (2895) · no failures. The same run re-ran `@objectstack/connector-mcp`'s suite on that head: 5 files / 56 tests passed. (An earlier battery at the pre-merge head `6504a2e9` read the same counts.) The derived gates below were run at `6504a2e9`; the merge brought only `main`'s own files, so the change set is the same four paths, and `dispatch-gates --ran` re-reconciled on `6840f48c`: 87 derived, 87 run, 0 NOT-MEASURED. <details><summary>Derived gates: 87 derived, 87 run, 0 NOT-MEASURED (dispatch-gates --ran, a derived zero: every row carries an exit code)</summary> All 87 exit 0. Two first answered exit 3 (`PREREQUISITE NOT MET`, not a verdict) before their dists existed — `check:skill-examples` (client-react `.d.ts`) and `check:dual-build-cjs-loads` (8 dists); after building those packages both reran exit 0 (`275 prose examples type-check`; cjs-loads floors held). Docs family readings for the `connectors.mdx` paragraph: `check:docs` ✅ 232 generated files in sync · `check:doc-anchors` ✅ 822 links resolve · `check:doc-authoring` ✓ baseline held · `check:doc-frontmatter` ✓ · `check:docs-single-h1` ✓ 472 pages · `check:docs-redirects` OK · `check:docs-spec-enumerations` OK · `check:docs-transcript-drift` ✓ · `check:doc-security-posture` ✅ · `check:doc-formula-expressions` exit 0 · `check-doc-route-spelling --advisory` ✓ · `docs-audit/check-affected-docs` exit 0 · `check-issue-citations` ✅ none added. ADR-0087: `check-adr-0087-registration --base origin/main` ✓ 1 declared-breaking changeset carrying a disposition (`not-required (no-migration-prescription)`); `check-changeset-no-major` ✓ no major. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgUmmMVhd9t2Y2dExvC)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgUmmMVhd9t2Y2dExvC --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1fa4800 commit 4694365

4 files changed

Lines changed: 230 additions & 3 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/connector-mcp': minor
3+
---
4+
5+
fix(connector-mcp)!: a declarative `mcp` instance refuses a `providerConfig.include` it cannot read, and an entry naming a tool the server does not list, instead of exposing every tool
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: `providerConfig` is an open record the `mcp` provider reads itself, and `include` keeps its name and its allowlist meaning, now the same as the `openapi` provider's. What changes is that two values the provider used to drop silently are refused at materialization, each with a message naming the entry and the fix, so there is nothing for a metadata migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing of `providerConfig.include` on `provider: 'mcp'` connector instances (`@objectstack/connector-mcp`).
13+
14+
- **Before.** An `include` that was not an array (a string such as `include: 'web_search'`, an object, `null`) was dropped and read as "no allowlist": the instance exposed **every** tool the server lists. Non-string entries were filtered out silently (`['web_search', 7]` exposed `web_search`). An entry naming a tool the server does not list was ignored: `include: ['nope']` brought the instance up `ready` with no actions and no error.
15+
- **Now.** `include` keeps the `openapi` provider's meaning: absent means every tool; otherwise it must be an array of non-empty tool names.
16+
- Any other shape is refused before connecting, with `connector-mcp provider: connector 'NAME' providerConfig.include must be an array of tool names (strings): ...`.
17+
- An entry the server's `tools/list` does not carry is refused after listing, naming the entry and the tools the server lists (`... providerConfig.include names 'nope', which the MCP server does not list. ...`); the connection is closed first.
18+
- Both are configuration faults, thrown plain: fatal at boot, a skipped entry on a reload. The instance never falls back to exposing every tool.
19+
- **Who sees it.** An app whose `mcp` connector instance writes `include` as anything but an array of tool names, or lists a tool its server does not (a typo, or a tool the server renamed or dropped).
20+
21+
**The remedy.** Write `include` as an array of the server's tool names (`include: ['web_search']`), or remove it to expose every tool. For an entry the refusal names, use a name from the tool list the refusal prints, or delete the entry.

‎content/docs/automation/connectors.mdx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,16 @@ connect, handshake or `tools/list`) is a configuration fault instead: the
231231
instance is degraded naming the refusal, as described under *A credential the
232232
upstream refuses* below.
233233

234+
`include` has the same allowlist meaning as the `openapi` provider's: list the
235+
tools the instance should expose and every other tool stays out — the way to
236+
keep a destructive tool away from your flows and agents. It must be an array of
237+
tool names; any other shape (a string such as `include: 'web_search'`, an
238+
object) fails the boot before connecting, and never falls back to exposing
239+
every tool. An entry the server's `tools/list` does not carry — a typo, or a
240+
tool the server renamed or dropped — fails the boot with an error naming the
241+
entry and listing the server's tools; it is never ignored. Leave `include` out
242+
to expose every tool.
243+
234244
<Callout type="warn">
235245
**Declarative stdio transports are denied by default.** Materializing one
236246
spawns a local process *from metadata* — which a runtime Studio publish can

‎packages/connectors/connector-mcp/src/mcp-provider.test.ts‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
// fake client here), listing its tools, and mapping them to actions.
66

77
import { describe, it, expect } from 'vitest';
8+
import { LiteKernel } from '@objectstack/core';
9+
import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/service-automation';
810
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
911
import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
1012
import { McpError } from '@modelcontextprotocol/sdk/types.js';
1113
import type { ConnectorProviderContext } from '@objectstack/spec/integration';
1214
import { isConnectorCredentialRejected, isConnectorUpstreamUnavailable } from '@objectstack/spec/integration';
1315
import type { McpClientLike, McpToolDescriptor, McpTransport } from './mcp-connector.js';
16+
import { ConnectorMcpPlugin } from './connector-mcp-plugin.js';
1417
import { createMcpProviderFactory, MCP_PROVIDER_KEY } from './mcp-provider.js';
1518

1619
const TOOLS: McpToolDescriptor[] = [
@@ -364,3 +367,143 @@ describe('mcp provider: declarative stdio working directory (#22423)', () => {
364367
expect(seen.transport).toBeUndefined();
365368
});
366369
});
370+
371+
// #27402 — `providerConfig.include` is the allowlist that keeps destructive
372+
// tools away from flows and agents, with the `openapi` provider's one meaning
373+
// (#26709, #27387): a shape it cannot read is refused before connecting, an
374+
// entry naming a tool the server does not list is refused after listing, and
375+
// the instance never falls back to exposing every tool.
376+
describe('mcp provider: providerConfig.include refuses loudly (#27402)', () => {
377+
const SERVER_TOOLS: McpToolDescriptor[] = [
378+
{ name: 'web_search', description: 'Search the web' },
379+
{ name: 'delete_everything', description: 'Destructive' },
380+
];
381+
const http = { kind: 'http', url: 'https://mcp.vendor.example/mcp' };
382+
383+
/** A fake server listing SERVER_TOOLS; records whether it was connected and closed. */
384+
function server() {
385+
const state = { connected: false, closed: false };
386+
const clientFactory = async (): Promise<McpClientLike> => {
387+
state.connected = true;
388+
return {
389+
listTools: async () => SERVER_TOOLS,
390+
callTool: async () => ({ content: [] }),
391+
close: async () => { state.closed = true; },
392+
};
393+
};
394+
return { clientFactory, state };
395+
}
396+
397+
async function refusal(include: unknown) {
398+
const { clientFactory, state } = server();
399+
const factory = createMcpProviderFactory({ clientFactory });
400+
const err: unknown = await Promise.resolve(
401+
factory(ctx({ name: 'vendor', providerConfig: { transport: http, include } })),
402+
).then(
403+
(mat) => { throw new Error(`expected a refusal, got actions ${Object.keys(mat.handlers).join(', ')}`); },
404+
(e: unknown) => e,
405+
);
406+
return { err, state };
407+
}
408+
409+
/** A configuration fault: plain, neither retryable nor a credential refusal. */
410+
function expectConfigurationFault(err: unknown) {
411+
expect(err).toBeInstanceOf(Error);
412+
expect(isConnectorUpstreamUnavailable(err)).toBe(false);
413+
expect(isConnectorCredentialRejected(err)).toBe(false);
414+
}
415+
416+
it.each([
417+
['a string', 'web_search'],
418+
['an array with a non-string entry', ['web_search', 7]],
419+
['an array with an empty name', ['web_search', '']],
420+
['an object', { web_search: true }],
421+
['null', null],
422+
])('refuses an include that is %s, before connecting', async (_shape, include) => {
423+
const { err, state } = await refusal(include);
424+
expectConfigurationFault(err);
425+
expect((err as Error).message).toMatch(
426+
/^connector-mcp provider: connector 'vendor' providerConfig\.include must be an array of tool names \(strings\)/,
427+
);
428+
expect(state.connected).toBe(false);
429+
});
430+
431+
it('refuses an entry naming a tool the server does not list, naming it and the listed tools, and closes the client', async () => {
432+
const { err, state } = await refusal(['nope']);
433+
expectConfigurationFault(err);
434+
expect((err as Error).message).toMatch(
435+
/^connector-mcp provider: connector 'vendor' providerConfig\.include names 'nope', which the MCP server does not list\./,
436+
);
437+
expect((err as Error).message).toContain("the server lists 2 tool(s): 'web_search', 'delete_everything'.");
438+
expect(state.closed).toBe(true);
439+
});
440+
441+
it('refuses when one entry is listed and another is not: no partial instance', async () => {
442+
const { err } = await refusal(['web_search', 'nope', 'nope']);
443+
expectConfigurationFault(err);
444+
expect((err as Error).message).toContain("providerConfig.include names 'nope', which");
445+
});
446+
447+
it('an include of listed tools exposes exactly those tools', async () => {
448+
const { clientFactory, state } = server();
449+
const mat = await createMcpProviderFactory({ clientFactory })(
450+
ctx({ name: 'vendor', providerConfig: { transport: http, include: ['web_search'] } }),
451+
);
452+
expect(Object.keys(mat.handlers)).toEqual(['web_search']);
453+
expect(mat.def.actions?.map((a) => a.key)).toEqual(['web_search']);
454+
expect(state.closed).toBe(false);
455+
});
456+
457+
it('control: no include exposes every tool the server lists', async () => {
458+
const { clientFactory } = server();
459+
const mat = await createMcpProviderFactory({ clientFactory })(
460+
ctx({ name: 'vendor', providerConfig: { transport: http } }),
461+
);
462+
expect(Object.keys(mat.handlers)).toEqual(['web_search', 'delete_everything']);
463+
});
464+
465+
// Through the materializer: a refused include fails the boot naming the
466+
// instance and the fault, and no connector exposing the server's tools is
467+
// ever registered under the declared name.
468+
it('a declarative instance with a string include fails boot, never registering every tool', async () => {
469+
const { clientFactory, state } = server();
470+
const declared = [
471+
{
472+
name: 'vendor_mcp',
473+
label: 'Vendor MCP',
474+
type: 'api',
475+
provider: 'mcp',
476+
providerConfig: { transport: http, include: 'web_search' },
477+
},
478+
];
479+
const kernel = new LiteKernel({ logger: { level: 'silent' } } as never);
480+
kernel.use(new AutomationServicePlugin());
481+
kernel.use({
482+
name: 'test.connector-metadata',
483+
type: 'standard',
484+
version: '1.0.0',
485+
dependencies: ['com.objectstack.service-automation'],
486+
async init(c: { registerService(name: string, service: unknown): void }) {
487+
c.registerService('objectql', {
488+
registry: { listItems: (type: string) => (type === 'connector' ? declared : []) },
489+
});
490+
},
491+
async start() {},
492+
} as never);
493+
kernel.use(new ConnectorMcpPlugin({ clientFactory }));
494+
try {
495+
const booted: unknown = await kernel.bootstrap().then(() => undefined, (e: unknown) => e);
496+
expect(booted).toBeInstanceOf(Error);
497+
expect((booted as Error).message).toContain(
498+
"failed to materialize connector instance 'vendor_mcp' via provider 'mcp'",
499+
);
500+
expect((booted as Error).message).toContain('providerConfig.include must be an array of tool names');
501+
expect(state.connected).toBe(false);
502+
const engine = kernel.getService('automation') as AutomationEngine | undefined;
503+
const descriptor = engine?.getConnectorDescriptors().find((d) => d.name === 'vendor_mcp');
504+
expect(descriptor?.actions ?? []).toEqual([]);
505+
} finally {
506+
await kernel.shutdown().catch(() => {});
507+
}
508+
});
509+
});

‎packages/connectors/connector-mcp/src/mcp-provider.ts‎

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,13 @@ async function credentialRejectionStatus(err: unknown): Promise<number | undefin
208208
* a configuration fault the materializer degrades the instance for (see
209209
* {@link credentialRejectionStatus}, #27237). A handshake that carried the
210210
* instance's credential answers `credentialVerified`.
211+
*
212+
* `providerConfig.include` (optional) narrows the actions to an allowlist of
213+
* tool names, with the `openapi` provider's meaning. An `include` that is not
214+
* an array of non-empty strings is refused before connecting, and an entry
215+
* naming a tool the server's `tools/list` does not carry is refused after
216+
* listing (the connection is closed first); both are configuration faults,
217+
* thrown plain. The instance never falls back to exposing every tool.
211218
*/
212219
export function createMcpProviderFactory(deps: McpProviderDeps = {}): ConnectorProviderFactory {
213220
return async (ctx) => {
@@ -222,10 +229,33 @@ export function createMcpProviderFactory(deps: McpProviderDeps = {}): ConnectorP
222229
transport = { ...transport, cwd: await ctx.resolvePackagePath('.') };
223230
}
224231
}
225-
const includeList = Array.isArray(cfg.include)
226-
? cfg.include.filter((x): x is string => typeof x === 'string')
232+
// `include` keeps the `openapi` provider's meaning (one allowlist, two
233+
// providers): absent ⇒ every tool; otherwise an array of non-empty tool
234+
// names. Any other shape is a configuration fault, thrown plain before
235+
// connecting — fatal at boot, a skipped entry on reload. An allowlist that
236+
// dropped a shape it could not read would expose every tool the server
237+
// has, the very ones the author meant to keep out of flows and agents.
238+
if (
239+
cfg.include !== undefined &&
240+
(!Array.isArray(cfg.include) ||
241+
cfg.include.some((entry) => typeof entry !== 'string' || entry.length === 0))
242+
) {
243+
throw new Error(
244+
`connector-mcp provider: connector '${ctx.name}' providerConfig.include must be an array of ` +
245+
`tool names (strings): each the name of a tool the MCP server lists in tools/list.`,
246+
);
247+
}
248+
const includeList = cfg.include as readonly string[] | undefined;
249+
// The allowlist rides the connector's own `include` predicate; every tool
250+
// name the server lists is recorded on the way, so an entry nothing
251+
// matched can be named in the refusal below.
252+
const listedTools: string[] = [];
253+
const include = includeList
254+
? (toolName: string) => {
255+
listedTools.push(toolName);
256+
return includeList.includes(toolName);
257+
}
227258
: undefined;
228-
const include = includeList ? (toolName: string) => includeList.includes(toolName) : undefined;
229259

230260
let bundle;
231261
try {
@@ -270,6 +300,29 @@ export function createMcpProviderFactory(deps: McpProviderDeps = {}): ConnectorP
270300
{ cause: err },
271301
);
272302
}
303+
if (includeList) {
304+
// An entry naming a tool the server does not list is a configuration
305+
// fault, thrown plain — fatal at boot, a skipped entry on reload — after
306+
// the connection is released. Ignoring it would bring the instance up
307+
// without the tool the author listed (a typo, or a tool the server
308+
// renamed or dropped), and the first sign would be a flow failing at
309+
// run time.
310+
const offered = new Set(listedTools);
311+
const unknown = [...new Set(includeList)].filter((entry) => !offered.has(entry));
312+
if (unknown.length > 0) {
313+
await bundle.close().catch(() => {});
314+
// At most 20 of the server's tool names, as the openapi refusal lists.
315+
const sample = [...offered].slice(0, 20);
316+
const more = offered.size > sample.length ? `, … ${offered.size - sample.length} more` : '';
317+
throw new Error(
318+
`connector-mcp provider: connector '${ctx.name}' providerConfig.include names ` +
319+
`${unknown.map((entry) => `'${entry}'`).join(', ')}, which the MCP server does not list. ` +
320+
`Each entry must be the name of a tool from the server's tools/list; ` +
321+
`the server lists ${offered.size} tool(s): ` +
322+
`${sample.map((name) => `'${name}'`).join(', ')}${more}.`,
323+
);
324+
}
325+
}
273326
// [#27237] The handshake carried the instance's credential only over
274327
// http, and only as a header (an api-key `paramName` is not sent): then
275328
// the server's answer proved it.

0 commit comments

Comments
 (0)