You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d925ac7
Browse filesBrowse the repository at this point in the historyBrowse files
fix(connector-mcp): an include a credentialed MCP server does not list degrades the instance as a credential refusal, not the boot (#28357)
Fixes#28311
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:MCP 连接器实例发送了凭据、而服务器的 `tools/list` 没有列出 `include`
点名的工具时,不再按“声明错误”让整个服务启动失败,而是按“凭据问题”把该实例降级,存储的凭据标为
`rejected`;没有凭据的实例行为完全不变。
- 为什么改:GitHub
这类服务器按令牌权限过滤工具列表,权限不够的令牌放在环境变量里会让整个运行时下次重启起不来,放在凭据存储里又提示管理员“去修声明”;能修的其实是凭据,而列表本身分不清是“被隐藏”还是“根本没有”,所以提示同时写明两种原因。
- 风险与回滚:只改变“实例发送了 `auth` 凭据且工具缺失”这一种情况的归类,复用已有的
`ConnectorCredentialRejectedError` 与 `rejected` 状态,没有新类型、新状态。回滚即还原
`mcp-provider.ts` 里的 `if (credentialSent)` 分支。
- 先看:`packages/connectors/connector-mcp/src/mcp-provider.ts`(`if
(credentialSent)` 分支与 403
的理由)、`packages/connectors/connector-mcp/src/mcp-include-credential-scope.test.ts`(环境变量路径与存储路径两条启动测试)、`content/docs/automation/connectors.mdx`。
- 两分钟验证:依赖闭包构建后运行 `pnpm --filter @objectstack/connector-mcp exec vitest
run src/mcp-include-credential-scope.test.ts`,预期 `Tests 9 passed (9)`。
- 需要你做的:无,席位落地。
## What changes
- **`@objectstack/connector-mcp` (`mcp-provider.ts`).** When
`providerConfig.include` names a tool the server's `tools/list` does not
carry, the refusal now depends on whether the handshake carried the
instance's credential (an `http` transport with the instance `auth` sent
as a header — the same predicate `credentialVerified` already used):
- **carried:** `ConnectorCredentialRejectedError`, status `403`. The
message names the missing tools, the tools the server lists for that
credential, and both causes with the fix for each (the credential lacks
the scopes those tools need, or the declaration names a tool the server
does not have). The materializer's existing credential path then
applies: degraded, boot not failed; a stored credential marked
`rejected` and held; an environment or vault credential retried on the
backoff.
- **not carried** (no `auth`, an `api-key` sent as a query parameter, a
`stdio` transport, or a credential written into
`providerConfig.transport.headers`, which is part of the declaration):
the plain configuration fault, byte-identical message, fatal at boot as
before.
- **Docs.** `content/docs/automation/connectors.mdx`: the `include`
paragraph, the "credential the upstream refuses" paragraph and two rows
of the failure-mode table. `packages/apps/connector-github/README.md`:
the Credentials paragraph no longer says an environment-variable token
stops the server from starting.
- **Contract text the change would otherwise make false.**
`ConnectorCredentialRejectedError` / `CONNECTOR_CREDENTIAL_REJECTED`
TSDoc in `packages/spec` (comments only, no type change) and the
`sys_connector_credential.status` field description now name the second
way the marker is raised.
- **Tests.** `src/mcp-include-credential-scope.test.ts` (new): the three
probes the card was filed from, made permanent on the real provider, the
real materializer and the real credential store (ObjectQL over SQLite),
plus controls. For the store, `connector-mcp` gains three
devDependencies (`@objectstack/objectql`, `@objectstack/driver-sql`,
`@objectstack/platform-objects`, all already in its build closure) and a
`vitest.config.ts` that reads them from source.
- **Changeset.** `@objectstack/connector-mcp` patch,
`@objectstack/service-automation` patch (the field description).
## Tests
At head `b6312fe6`:
- `pnpm --filter @objectstack/connector-mcp test`: `Test Files 6 passed
(6)`, `Tests 65 passed (65)`; `typecheck` exit 0, test-typecheck ledger
unchanged (`1 file(s) / 5 error(s)`, the new file adds none).
- `pnpm --filter @objectstack/connector-app-github test` (validate +
vitest): `Validation passed`, `Test Files 2 passed (2)`, `Tests 10
passed (10)`; `typecheck` exit 0.
- `@objectstack/service-automation` (the description edit), at
`c7628191`, no later commit touches it: `Test Files 237 passed (237)`,
`Tests 2985 passed (2985)`; `typecheck` exit 0.
- The new file, 9 cases: credentialed refusal (403, both causes, missing
and listed tools, client closed, no token in the message); controls — no
`auth`, `api-key` as a query parameter, a credential in
`transport.headers`, `stdio` with `auth`, and a credential whose listing
carries every entry (materializes, `credentialVerified`); through the
materializer — environment credential (boot not failed, degraded, listed
`rejected` / `env`, not held, recovers on a reconcile once the same
token's scopes cover the tools), stored credential (degraded with the
credential reason, row `rejected` with that reason, held as
`credential`, a reconcile dials nothing, a better-scoped value recovers
it with `lastVerifiedAt` set; neither token in any log line; no "Fix its
declaration" line), and no credential (boot still fails with the
declaration fault).
- **Ablation, once, at `a51f1291`:** `node scripts/ablation-replace.mjs`
replaced `if (credentialSent) {` with `if (false as boolean) {` (anchor
1 to 0, blob `05b010b670de` to `2dd4cd2773e3`): `Tests 3 failed | 6
passed (9)`, the three credential-path cases red, every control green;
restored, blob equals HEAD, `git diff HEAD` empty. The test imports the
provider by relative path from `src`, so no build sits between the
mutation and the run.
## Acceptance notes
- **Can the server tell the two causes apart? Measured: no.**
`github/github-mcp-server` at `f2a07cd1`: the scope filter is applied to
the inventory before `tools/list` (`pkg/http/handler.go`
`PATScopeFilter`, `pkg/github/scope_filter.go`), the result carries no
field about hidden tools, and the HTTP layer sets no scope header on
that answer. The only scope signal is `WWW-Authenticate: Bearer
error="insufficient_scope"` with `403` on a `tools/call`
(`pkg/http/middleware/scope_challenge.go`). So the message names both
causes, and status `403` follows that same convention (RFC 6750 section
3.1); the message itself quotes no HTTP status.
- **The GitHub app's own two tools are never hidden by GitHub's
server.** At `f2a07cd1`, `search_issues` and `issue_read` are declared
with `scopes.PublicRead(scopes.Repo)`, whose visibility check returns
true for any scopes (`pkg/scopes/scopes.go`, `PublicRead`), matching
GitHub's `docs/scope-filtering.md` "Public Repository Access". The
defect is reachable with GitHub through other tools (write tools,
`notifications`, project and org tools) and with any server that filters
by credential. The README paragraph now says so.
- **Row state.** The existing `rejected` state is reused; no new state.
It clears when the value is set again or a handshake lists every entry
(`markVerified`). A credentialed instance whose `include` has a real
typo now marks its stored credential `rejected` too; the message names
that cause, and editing the declaration re-attempts it and clears the
mark on success.
- **Where the probes live.** In `connector-mcp`'s own tests, which run
without the app; `packages/apps/connector-github/test/boot.test.ts` is
unchanged.
- `packages/services/service-automation/src/plugin.ts` is not touched.
Its hold log line ("its upstream refused its credential") reads
acceptably for this case and is left alone.
- `origin/main` was merged at `efdddc90`, after #28306 landed. Main has
moved since; GitHub's merge of the newer commits is clean (`git
merge-tree` exit 0), and none of them touches these packages.
## Verification
<details><summary>Gate families derived for this diff: 114 run, 0 NOT
MEASURED</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `b6312fe6`: 114 commands for 10 paths,
569 changed lines (+533 / -36) (under the 3000 threshold). Every command
was run with its exit code captured before any pipe. 112 exited 0 on the
first pass. `pnpm --filter @objectstack/spec run check:skill-examples`
and `pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET:
unbuilt `client-react` and siblings). Re-run once the dists existed,
both exited 0 (`277 prose examples type-check across 3 surface(s)`; `113
published require entry point(s) across 70 package(s) load`). `--ran`
over the final record: `114 derived famil(ies) accounted for — 114 run,
0 NOT-MEASURED`.
</details>
<details><summary>Not run locally, by rule</summary>
`test:repo`, `pnpm lint` and the other repo-wide scans belong to CI.
Dogfood does not apply: no `!` in the title and no `(narrowing)` arm.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(connector-mcp): when a server that was sent the instance's credential does not list an `include`d tool, the instance is degraded as a credential refusal instead of failing the boot as a declaration fault
7
+
8
+
Clause-②: no
9
+
Metadata-protocol: none
10
+
11
+
Some MCP servers list only the tools the credential they are sent may use: GitHub's hides each tool a classic personal access token's scopes do not cover. Its `tools/list` answer says nothing about what it hid, so a tool missing from it may be hidden from this credential or may not exist on the server at all.
12
+
13
+
-**Before.** When `providerConfig.include` named a tool the server did not list, the `mcp` provider refused with a plain configuration fault in every case. With the credential in an environment variable, the whole boot failed. With a credential stored through the credential door, the instance was degraded with a reason telling the administrator to fix the app's declaration.
14
+
-**Now.** When the handshake carried the instance's `auth` credential (an `http` transport, the credential sent as a header), the refusal is a `ConnectorCredentialRejectedError` with status `403`. Its message names the missing tools and the tools the server does list for that credential, and it names both causes with the fix for each: give the credential the scopes those tools need and set it again, or fix `providerConfig.include`. The materializer handles it as it handles a `401` / `403`: the instance is degraded and the boot does not fail. A stored credential is marked `rejected` and held until it is set again; an environment or vault credential is retried on the backoff, so widening the token's scopes where it was issued recovers the instance without a restart.
15
+
-**Unchanged.** An instance whose handshake carried no static credential (no `auth`, an `api-key` sent as a query parameter, a `stdio` transport, or a grant-bearing `oauth2` / `app-credential` instance, whose token is borrowed per request from an organization's connection) keeps the plain configuration fault, fatal at boot, with the same message as before.
16
+
-**Where it reads.** The `sys_connector_credential.status` field description (`@objectstack/service-automation`) now names this second way a stored credential becomes `rejected`.
Copy file name to clipboardExpand all lines: content/docs/automation/connectors.mdx
+18-6Lines changed: 18 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -250,9 +250,19 @@ keep a destructive tool away from your flows and agents. It must be an array of
250
250
tool names; any other shape (a string such as `include: 'web_search'`, an
251
251
object) fails the boot before connecting, and never falls back to exposing
252
252
every tool. An entry the server's `tools/list` does not carry — a typo, or a
253
-
tool the server renamed or dropped — fails the boot with an error naming the
254
-
entry and listing the server's tools; it is never ignored. Leave `include` out
255
-
to expose every tool.
253
+
tool the server renamed or dropped — is never ignored: the refusal names the
254
+
entry and lists the server's tools. On an instance that sends no static
255
+
credential — no `auth`, an `api-key` sent as a query parameter, a `stdio`
256
+
transport, or a grant-bearing `oauth2` / `app-credential` instance, whose token
257
+
is borrowed per request — it fails the boot. When the instance sends a static
258
+
`auth` credential (`bearer`, `basic`, or an `api-key` header), the entry may
259
+
instead be a tool the server hides from that credential: some servers list
260
+
only the tools a credential is allowed to use (GitHub's hides each tool a
261
+
classic token's scopes do not cover), and the answer does not say what it hid.
262
+
That instance is degraded as a credential the upstream refuses (below), never
263
+
failing the boot, and its reason names both causes: a credential without the
264
+
scopes those tools need, or a declaration naming a tool the server does not
265
+
have. Leave `include` out to expose every tool.
256
266
257
267
<Callouttype="warn">
258
268
**Declarative stdio transports are denied by default.** Materializing one
@@ -378,7 +388,9 @@ plus a `credentialNote` naming the ones still to set.
378
388
379
389
**A credential the upstream refuses** (HTTP `401` / `403`) degrades the
380
390
instance, with a reason quoting the upstream's message, never the value. An
381
-
`mcp` instance finds this out when it connects. A `rest` or `openapi` instance
391
+
`mcp` instance finds this out when it connects, and treats a server that
392
+
accepts the credential but does not list a tool its `include` names the same
393
+
way. A `rest` or `openapi` instance
382
394
finds out on the first call answering `401` / `403`; that call returns its
383
395
answer as usual, and the instance is degraded after it. What happens next
384
396
depends on where the value came from:
@@ -768,11 +780,11 @@ retry):
768
780
|`credentialRef` (or a `signed` instance's `vendorPublicKeyRef`) resolves to nothing |**Not fatal**: the instance registers **degraded**, naming the reference — set it (`PUT …/credential-refs/:ref`), and it materializes without a restart |
769
781
| A `signed` reference resolves to something that is not a key of the scheme's kind (an `alipay-v3` key that is not RSA) |**Hard boot error** naming the instance and which reference cannot sign or verify, never the value |
770
782
| A response to an `alipay-v3` call is unsigned (below status 400) or its signature does not verify | The dispatch **fails** naming the instance and the status; the response's body is never returned |
771
-
| The upstream refuses the credential (`401` / `403`) |**Not fatal**: the instance registers **degraded**. A stored credential is marked `rejected` and waits until it is set again (or a restart); an environment or vault credential is retried on the backoff |
783
+
| The upstream refuses the credential (`401` / `403`), or an `mcp` server sent a static credential does not list a tool the instance's `include` names|**Not fatal**: the instance registers **degraded**. A stored credential is marked `rejected` and waits until it is set again (or a restart); an environment or vault credential is retried on the backoff |
772
784
| Instance `name` collides with a plugin-registered connector |**Hard boot error** — there is no silent precedence between the two worlds |
773
785
| stdio transport without host opt-in |**Hard boot error** with the `declarativeStdio` fix inline — a security rejection is never retried |
774
786
| MCP server unreachable / `tools/list` fails; remote spec URL unreachable or transiently failing (408/429/5xx) |**Not fatal**: the instance registers **degraded** (visible on `GET /api/v1/automation/connectors` with the reason), dispatches fail with that reason, and the platform retries on a 5s→5min backoff — recovery is automatic |
775
-
| A retry of a degraded instance reaches the upstream and meets a configuration fault (an `include` naming a tool or operation the upstream does not offer) |**Not fatal**: the instance stays **degraded**, now with the configuration reason, and is **not retried** — dispatches fail saying to edit its declaration; an edit of the entry (or a restart) re-attempts it |
787
+
| A retry of a degraded instance reaches the upstream and meets a configuration fault (an `include` naming a tool or operation the upstream does not offer; on an `mcp` instance that sends a static credential, the credential row above applies instead) |**Not fatal**: the instance stays **degraded**, now with the configuration reason, and is **not retried** — dispatches fail saying to edit its declaration; an edit of the entry (or a restart) re-attempts it |
| A `connector_action` input that, once evaluated, breaks the action's `inputSchema` (a missing `required` parameter, a wrong `type`) |**The step fails before dispatch** with `VALIDATION_ERROR`, naming each parameter path, and nothing is sent; a `fault` edge or `try_catch` can route it. Supply the parameter or send the declared type: text is never converted, so cast it in a CEL envelope (`int()`, `double()`, `bool()`). `GET /api/v1/automation/connectors` lists the schema; `os validate` does not judge the input against the `inputSchema` yet |
778
790
| A bad entry arrives via **runtime reload** (a Studio publish, a direct active save through `PUT /api/v1/meta/connector/:name`, or a delete, made on any replica — peers reconcile on [cluster lane 2](/docs/kernel/cluster#62-the-cluster-invalidation-family); or a dev reload) | Logged and **skipped**, never crashing the live server; a changed instance's old connector keeps serving until its replacement materializes |
Copy file name to clipboardExpand all lines: packages/apps/connector-github/README.md
+12-7Lines changed: 12 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,13 +34,18 @@ install dialog right after installing. An environment variable named
34
34
`GITHUB_MCP_TOKEN` on the server takes precedence over a stored value.
35
35
36
36
Prefer a fine-grained token. With a classic token, GitHub's server hides every
37
-
tool whose scope the token lacks. If that hides `search_issues` or
38
-
`issue_read`, the connector does not start without them: a value set in
39
-
**Connector Credentials** leaves `github_mcp` disconnected with a reason naming
40
-
the missing tools, and a value set as the server environment variable stops the
41
-
server from starting at all. Replace the token with one that carries the `repo`
42
-
scope: a new value in **Connector Credentials** takes effect at once, a new
43
-
environment variable at the next start.
37
+
tool whose scope the token lacks, though it keeps read tools that also work on
38
+
public repositories listed whatever the scopes, `search_issues` and
39
+
`issue_read` among them ([PAT scope filtering](https://github.com/github/github-mcp-server/blob/main/docs/scope-filtering.md#public-repository-access)).
40
+
If the server does leave out a tool this app includes, `github_mcp` stays
41
+
disconnected with a reason naming the missing tools and both possible causes,
42
+
the token's scopes or the app's declaration, and its flows fail; the server
43
+
itself keeps starting, wherever the value is set. Replace the token with one
44
+
that carries the `repo` scope: a new value in **Connector Credentials** takes
45
+
effect at once, a new environment variable at the next start. Widening the
46
+
scopes of the token you already use works too: set it again in **Connector
47
+
Credentials**, or, for the environment variable, wait for the connector's next
48
+
automatic retry, at most five minutes away.
44
49
45
50
The token is only ever sent to GitHub. It is never stored in the app's
0 commit comments