Skip to content

fix(connector-mcp)!: refuse an unreadable or unknown providerConfig.include instead of exposing every tool - #27985

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-it3g43
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-it3g43

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Requested by Sales · project thread

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(feat(connector-openapi): declarative providerConfig.include operation allowlist #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.
Red first (fix stashed, base ed0e71d): 8 failed / 26 passed
× 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)
Green and ablation (scripts/ablation-replace.mjs, each leg restored blob == HEAD ae66f3def905, git diff HEAD empty)
  • 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.

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 feat(connector-openapi): declarative providerConfig.include operation allowlist #27387 made false.
  • git merge-tree against every open PR touching packages/connectors/connector-mcp/** or content/docs/automation/connectors.mdx: only chore: version packages #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.

Derived gates: 87 derived, 87 run, 0 NOT-MEASURED (dispatch-gates --ran, a derived zero: every row carries an exit code)

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.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUmmMVhd9t2Y2dExvC

…nclude instead of exposing every tool

A declarative mcp instance read an include that was not an array as "no
allowlist" and exposed every tool the server lists, dropped non-string
entries, and ignored an entry naming a tool the server does not list.
include now keeps the openapi provider's meaning: any shape other than an
array of non-empty tool names is refused before connecting, and an entry
the server's tools/list does not carry is refused after listing (the
connection closed first). Both are plain configuration faults.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgUmmMVhd9t2Y2dExvC
…ape and an unknown tool

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgUmmMVhd9t2Y2dExvC
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a5396b81e0666dc4c71d13394a32e59ed411a48 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 055c30915d41654851883507bb1528287e23cb84 — the merge of head 6840f48cab7173ab94431a1a8c3b9b5af8bcc201 into base 4a5396b81e0666dc4c71d13394a32e59ed411a48, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 055c30915d41654851883507bb1528287e23cb84 && git checkout 055c30915d41654851883507bb1528287e23cb84
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a5396b81e0666dc4c71d13394a32e59ed411a48 6840f48cab7173ab94431a1a8c3b9b5af8bcc201 && git checkout -B drift-repro 4a5396b81e0666dc4c71d13394a32e59ed411a48 && git merge --no-ff 6840f48cab7173ab94431a1a8c3b9b5af8bcc201

node scripts/docs-audit/affected-docs.mjs --json 4a5396b81e0666dc4c71d13394a32e59ed411a48

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head 6840f48 · 2026-10-30T06:25Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6840f48cab7173ab94431a1a8c3b9b5af8bcc201
Local-runs: none
Scope: packages/connectors/connector-mcp/src/mcp-provider.ts, packages/connectors/connector-mcp/src/mcp-provider.test.ts, .changeset/27402-mcp-include-refuses.md, content/docs/automation/connectors.mdx + packages/connectors/connector-mcp/src/mcp-connector.ts, packages/connectors/connector-mcp/src/connector-mcp-plugin.ts, packages/connectors/connector-openapi/src/openapi-provider.ts, packages/connectors/connector-openapi/src/openapi-provider.test.ts, packages/services/service-automation/src/plugin.ts, packages/spec/src/integration/connector.zod.ts, packages/spec/src/integration/connector-provider.ts, docs/adr/0097-declarative-connector-instances.md

Read at 2026-10-30T06:37Z: card #27402 (body and all 7 comments, above all triage 6489254710, claim 6515507103 with amendments 6515533290 and 6515646431, os-dev-report 6516451168), PR #27985 (body, 4-file list, both comments, Contract-review-claim 6516467804), the net diff of the head against main (4 files, +230/−3), the head's check-runs, and the cited code at the head. Adversarial read; the seat's conclusions were not taken as inputs.

① Derived judgments

Fail-closed, every shape traced through the factory at the head. The predicate is: refuse when include is not undefined and is either not an array or has an entry that is not a non-empty string. So include: 'web_search' (string), an object, null, a Set, a boxed string, ['web_search', 7], ['web_search', ''] are all refused before any client is created (the client factory is never called; the test pins connected === false). Explicit include: undefined reads as absent, which is the openapi provider's reading too and the only reading JSON metadata can carry. include: [] passes the shape check, the predicate denies every tool, the unknown set is empty, and the instance materializes with zero actions: the closed direction, as openapi. Whitespace variants, case variants and typos pass the shape check and are then refused after tools/list as unknown entries (exact-match against the names the server listed); duplicates are collapsed through a Set before the unknown check, so ['web_search', 'nope', 'nope'] names 'nope' once. A paginating server is not a hole: McpClientLike.listTools() is contracted to return every page, and the default client follows nextCursor up to the 100-page bound and otherwise throws, which createMcpConnector closes the client on and the factory classifies upstream-unavailable, so the predicate always sees the whole list or no instance exists. A tools/list that throws is unchanged (closed by createMcpConnector, rethrown as upstream-unavailable or credential-rejected); one that returns nothing with a non-empty include is refused with every entry named. No path yields every tool once include is present.

No leaked client on the refusal path. Shape refusal: no client. Unknown-entry refusal: bundle.close() is awaited, its own rejection swallowed, before the throw; the unknown-entry test pins closed === true, the listed-tools test pins closed === false; the dev's third ablation leg (close deleted, 1 failure) proves the pin is live.

Fidelity to openapi, read side by side. Shape predicate: identical (raw === undefined passes; !Array.isArray(raw) || raw.some(not a non-empty string) refuses). Fault class: both a plain Error, so neither isConnectorUpstreamUnavailable nor isConnectorCredentialRejected matches, and the materializer's fail() branch takes it. Message skeleton: connector-NAME provider: connector 'X' providerConfig.include must be an array of … (strings): each … and … providerConfig.include names 'a', 'b', which the … does not …. Each entry must be …; the … N …: 'k1', 'k2'…, … M more. with the same sample of 20 and the same … N more tail. Timing: openapi validates shape after its baseUrl check and before the document fetch; mcp validates after transport normalization and the stdio policy and before connecting. Both check unknown entries after the upstream list is in hand, through the same device (the generator's own include predicate records every offered key as it filters). The one asymmetry is necessary: mcp holds a live connection at that point and closes it; openapi holds none.

Materializer and the "rejected with the reason" clause. reconcileDeclaredConnectors (service-automation plugin.ts) catches the factory's throw, strips credential forms, and routes: credential-rejected → hold; upstream-unavailable → degrade; anything else → fail('[Automation] failed to materialize connector instance NAME via provider mcp (ADR-0097).', thrown), which at boot throws with the cause appended (the kernel boot test pins both halves of that message) and on reload logs at error and skips the entry. The teardown of a previous bundle happens only after a successful re-materialization, so on reload the prior valid connector keeps serving. That is the openapi provider's exact path and ADR-0097's recorded reconcile rule, and the docs page's reload table row says the same. There is no rejected instance state anywhere in the materializer; the word names a credential-row status. See ③ for what that leaves open.

Spec. providerConfig is z.record(z.string(), z.unknown()) with the describe text "validated by the provider factory at boot"; ADR-0097 §2 places validation in the factory. No schema moves; Metadata-protocol: narrowed is the right kind because an authorable value an author could write is now refused where that value is judged.

Tests. Every refusal is pinned through the real createMcpProviderFactory with an injected client that lists web_search + delete_everything, each asserting a plain fault (not upstream, not credential), the first sentence of the message, and the connection fact. The control (no include → both tools) and the positive case (['web_search'] → exactly that tool, not closed) are present. One pin runs through the real door: LiteKernel + AutomationServicePlugin + ConnectorMcpPlugin with a declared instance carrying a string include fails bootstrap() with the materializer's message plus the include message and never connects. Red-first 8 failed / 26 passed on base with the fix stashed; green 34/34; the 3-leg ablation isolates distinct assertion sets (shape off → 6, unknown off → 2, close off → 1), so each leg is meaningful.

Gates and dogfood. Check-runs on the head at this read: 33 success, 14 skipped, 6 in progress (Test Core shards 1, 2, 3, 5, 6, 8). Green: Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, Test UI, Live E2E UI, Lint · PM dispatch-gates, Spec property liveness, Check Changeset, Dogfood Regression Gate, PR-body guards. The dogfood battery is on record at this head: os-dev-report 6516451168 names the head's short id with command-exit 0, 357 files passed / 3 skipped, 2875 tests passed / 20 skipped; the head's merge commit was made at 2026-10-30T05:57Z and the report posted at 2026-10-30T06:24Z, which fits the 1435 s hold it reports. Their conclusions are the gate verdicts; nothing was re-run here.

② Semver level

@objectstack/connector-mcp: minor is right. The kind is narrowed (a value an author can write is now refused), and a protocol kind takes at least minor on the package the diff moves; no major (the dev's check-changeset-no-major reading, and the Check Changeset run is green). The changeset carries the ! title, the BREAKING banner, Before / Now / Who sees it / The remedy, both line-initial declarations (Clause-②: no (narrowing), Metadata-protocol: narrowed), and one ADR-0087 marker, not-required (no-migration-prescription), which is the honest disposition: a conversion rewriting a string into a one-element array would be the lenient alias the #26709 ruling refuses ("one meaning for include"), and an unknown tool name has no mechanical TO. The category is in the gate's CATEGORIES and ADR-0087's examples; the identical form with a "The remedy" paragraph landed on #27541. Precedent for the level: the openapi include changeset on #27387 was minor as added. PR body and changeset declarations agree.

③ Boundary flags

  • Deviation from the triage wording, accepted. Triage 6489254710 (2026-10-29T06:00Z) says "leave the instance rejected with the reason". No such instance state exists; the PR lands the clause as the configuration-fault path, exactly as openapi: boot fails naming the instance and the reason; a reload logs the reason at error and skips the entry. The dispatch's file surface excluded the materializer, and "exactly as openapi does" governs. The dev disclosed it in the acceptance notes.
  • Finding for the seat to file (outside the surface; both providers). A configuration fault that first surfaces during a degraded-instance backoff retry (server down at boot → husk; server up later → an include entry it does not list) reaches the soft fail() branch, which logs and continues but leaves degradedInstances and the registered husk untouched: GET /connectors keeps the stale "could not reach its MCP server" reason, the dispatch refusal keeps promising an automatic retry, and the timer reconnects every 5 minutes against a fault no retry can clear. Location: reconcileDeclaredConnectors, the catch's plain-error branch. Class: ADR-0097 contract (configuration faults are not retried). Fail-closed holds throughout; nothing is exposed. The openapi provider has the same gap (a spec URL transiently down at boot).
  • Open policy question (ruling, not this PR). On a reload, an edit that adds include to remove a destructive tool but mis-spells it fails to re-materialize, and ADR-0097's reconcile rule keeps the previous, wider connector serving, with the error logged. That is the rule as written and openapi behaves the same; whether a failed narrowing should degrade instead is for the ADR.
  • Spec stays open by design. providerConfig is an open record and no publish-time lint reads include's shape, so the first signal for a string include is boot or reload. ADR-0097 §2 puts the validation in the factory; noted, not a defect of this PR.
  • Out of scope, confirmed stale at the head (skills lane; the 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. The MCP recipe in the same file writes include: ['search_issues'] and stays valid.
  • Declared deviation, cosmetic. The PR-body footer carries the card-thread session link plus the harness block, with the Fixes #27402 line under the attribution block; the recognisers read the line-initial declarations and the PR-body guards are green.
  • Docs drift check listed no page ("not a clean bill of health"); the hand re-read here covers the provider: 'mcp' section and the reload table row of the same page, both consistent with the code.
  • Six Test Core shards were still running at the read; the Tier S landing waits on every check being green regardless of this record.

④ Design judgment

  • packages/connectors/connector-mcp/src/mcp-provider.ts — holds against createMcpConnector (one include call per listed tool, so the recorded list is the whole list), against the openapi provider it mirrors, and against the materializer's fail() branch. No must-fix. Should-fix (low, parity nits): name the sample size as a constant as openapi's INCLUDE_REFUSAL_KEY_SAMPLE does instead of a bare literal with a comment; and let the McpProviderConfig.include member doc say what the factory doc now says, as openapi's interface doc does.
  • packages/connectors/connector-mcp/src/mcp-provider.test.ts — the refusals are pinned at the factory with the connection facts, the control is present, the ablation is meaningful. No must-fix. Should-fix: (a) pin include: [] → zero actions and a materialized instance, the claim the PR body makes and the openapi test names mcp for, with no mcp pin on the head; (b) pin the unknown-entry refusal through the real boot as openapi does (bootstrap() rejects with the names-entry message and getConnectorDegradedReason('vendor_mcp') is undefined, so the fault fails rather than degrades), and tighten the string-shape boot test's tail, whose descriptor?.actions ?? [] would pass vacuously if the engine were absent.
  • .changeset/27402-mcp-include-refuses.md — level, banner, declarations, disposition and remedy hold against the rule and the two precedents. No must-fix, no should-fix.
  • content/docs/automation/connectors.mdx — the new paragraph states the shape refusal before connecting, the unknown-entry refusal after listing, the never-every-tool guarantee and the absent-means-every-tool default, matching the code and the openapi paragraph above it; the page's existing reload row carries the reload semantics. No must-fix, no should-fix.

Implemented-by: claude/project-thread-it3g43
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:声明式 provider: 'mcp' 连接器的 providerConfig.include 写错时,实例不再静默暴露服务端的全部工具。有两种拒收:include 不是非空字符串数组时,连接前就拒收;条目点名了服务端 tools/list 里没有的工具时,列出后拒收(先关连接)。两种都按普通配置错误处理,消息形状与 openapi provider(feat(connector-openapi): declarative providerConfig.include operation allowlist #27387)一致。
  • 为什么改:include 是作者用来把危险工具挡在 flow 和 agent 之外的白名单。以前少打一对方括号(写成字符串),就会暴露全部工具,任何地方都不报错。
  • 风险与回滚:受影响的只有 include 写错的实例:启动时会失败,热重载时会记错误日志并跳过该条,旧实例照旧服务(ADR-0097 的 reconcile 规则,与 openapi 相同)。回滚 = revert 本 PR。
  • 先看:packages/connectors/connector-mcp/src/mcp-provider.ts 的 createMcpProviderFactory。
  • 两分钟验证:pnpm --filter @objectstack/connector-mcp exec vitest run src/mcp-provider.test.ts。
  • 需要你做的:无,由席位落地。

席位意见: ACCEPT。收窄与 openapi 逐项对齐,所有探过的形状都失败即关闭,契约复核 PASS。

席位核对

ACCEPT · head 6840f48cab7173ab94431a1a8c3b9b5af8bcc201 · 2026-10-30T06:40Z

  • 形态: 4 files (+230/−3): createMcpProviderFactory, its tests, the changeset, and the mcp section of content/docs/automation/connectors.mdx (claim amendment 6515646431). Tier S. No packages/spec.
  • Clause-②: no (narrowing). The title has !, the changeset has a BREAKING banner, minor on @objectstack/connector-mcp, and ADR-0087 not-required (no-migration-prescription). Metadata-protocol: narrowed.
  • 前提实测: the card's three shapes reproduced red before the fix (8 red), per the os-dev-report 6516451168.
  • 抽查读数: contract review 6516635534 PASS on this head. Fail-closed holds for every probed shape (string, object, null, empty or non-string entries, case/whitespace typos, duplicates, paginated or throwing tools/list). include: [] gives zero tools. No client is leaked on either refusal. Parity with openapi: the same predicate, fault class, message skeleton and timing. Narrowing dogfood battery at this head: 357 files passed, 2875 tests passed.
  • 门禁 check-runs: 54 on this head: 40 success, 14 skipped, 0 other (read 2026-10-30T06:40Z). Derived gates 87/87 exit 0, 0 NOT-MEASURED.
  • 接受的偏差: the review's three non-blocking should-fixes: no unit pin for include: []; the unknown-entry refusal is pinned at the factory, not through a LiteKernel boot, and the string-shape boot test's tail can pass vacuously; parity nits (sample-size constant, include member JSDoc). Triage's "rejected with the reason" maps to the configuration-fault path, because no rejected instance state exists outside credential rows. The dev disclosed this, and openapi does the same. The PR body's attribution footer names the card thread's session.
  • 范围外去向: service-automation: a configuration fault first seen on a degraded connector's backoff retry is logged and skipped, but the instance stays degraded with the stale "could not reach" reason and keeps retrying every 5 minutes #27992 (a configuration fault first seen on a degraded instance's backoff retry leaves a stale degraded state, on both providers; it also carries the reload policy question). skills(objectstack-automation): integration-recipes and its eval still say the openapi provider has no include key; since #27387 it has one, with the mcp allowlist meaning #27987 (skills text and its eval say openapi has no include).
  • 散文面: the changeset, the connectors.mdx section and the refusal messages, read by the seat and the review.
  • 落地条款: relay pr_ready + automerge_enable through the merge queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant