Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/28311-mcp-include-scope-hidden-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@objectstack/connector-mcp': patch
'@objectstack/service-automation': patch
---

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

Clause-②: no
Metadata-protocol: none

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.

- **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.
- **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.
- **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.
- **Where it reads.** The `sys_connector_credential.status` field description (`@objectstack/service-automation`) now names this second way a stored credential becomes `rejected`.
24 changes: 18 additions & 6 deletions content/docs/automation/connectors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,19 @@ keep a destructive tool away from your flows and agents. It must be an array of
tool names; any other shape (a string such as `include: 'web_search'`, an
object) fails the boot before connecting, and never falls back to exposing
every tool. An entry the server's `tools/list` does not carry — a typo, or a
tool the server renamed or dropped — fails the boot with an error naming the
entry and listing the server's tools; it is never ignored. Leave `include` out
to expose every tool.
tool the server renamed or dropped — is never ignored: the refusal names the
entry and lists the server's tools. On an instance that sends 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 — it fails the boot. When the instance sends a static
`auth` credential (`bearer`, `basic`, or an `api-key` header), the entry may
instead be a tool the server hides from that credential: some servers list
only the tools a credential is allowed to use (GitHub's hides each tool a
classic token's scopes do not cover), and the answer does not say what it hid.
That instance is degraded as a credential the upstream refuses (below), never
failing the boot, and its reason names both causes: a credential without the
scopes those tools need, or a declaration naming a tool the server does not
have. Leave `include` out to expose every tool.

<Callout type="warn">
**Declarative stdio transports are denied by default.** Materializing one
Expand Down Expand Up @@ -378,7 +388,9 @@ plus a `credentialNote` naming the ones still to set.

**A credential the upstream refuses** (HTTP `401` / `403`) degrades the
instance, with a reason quoting the upstream's message, never the value. An
`mcp` instance finds this out when it connects. A `rest` or `openapi` instance
`mcp` instance finds this out when it connects, and treats a server that
accepts the credential but does not list a tool its `include` names the same
way. A `rest` or `openapi` instance
finds out on the first call answering `401` / `403`; that call returns its
answer as usual, and the instance is degraded after it. What happens next
depends on where the value came from:
Expand Down Expand Up @@ -768,11 +780,11 @@ retry):
| `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 |
| 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 |
| 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 |
| 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 |
| 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 |
| Instance `name` collides with a plugin-registered connector | **Hard boot error** — there is no silent precedence between the two worlds |
| stdio transport without host opt-in | **Hard boot error** with the `declarativeStdio` fix inline — a security rejection is never retried |
| 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 |
| 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 |
| 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 |
| Wrong spec URL (other 4xx) or unparseable document | Configuration fault — **hard boot error** |
| 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 |
| 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 |
Expand Down
19 changes: 12 additions & 7 deletions packages/apps/connector-github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@ install dialog right after installing. An environment variable named
`GITHUB_MCP_TOKEN` on the server takes precedence over a stored value.

Prefer a fine-grained token. With a classic token, GitHub's server hides every
tool whose scope the token lacks. If that hides `search_issues` or
`issue_read`, the connector does not start without them: a value set in
**Connector Credentials** leaves `github_mcp` disconnected with a reason naming
the missing tools, and a value set as the server environment variable stops the
server from starting at all. Replace the token with one that carries the `repo`
scope: a new value in **Connector Credentials** takes effect at once, a new
environment variable at the next start.
tool whose scope the token lacks, though it keeps read tools that also work on
public repositories listed whatever the scopes, `search_issues` and
`issue_read` among them ([PAT scope filtering](https://github.com/github/github-mcp-server/blob/main/docs/scope-filtering.md#public-repository-access)).
If the server does leave out a tool this app includes, `github_mcp` stays
disconnected with a reason naming the missing tools and both possible causes,
the token's scopes or the app's declaration, and its flows fail; the server
itself keeps starting, wherever the value is set. Replace the token with one
that carries the `repo` scope: a new value in **Connector Credentials** takes
effect at once, a new environment variable at the next start. Widening the
scopes of the token you already use works too: set it again in **Connector
Credentials**, or, for the environment variable, wait for the connector's next
automatic retry, at most five minutes away.

The token is only ever sent to GitHub. It is never stored in the app's
metadata.
Expand Down
3 changes: 3 additions & 0 deletions packages/connectors/connector-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
"@objectstack/types": "workspace:*"
},
"devDependencies": {
"@objectstack/driver-sql": "workspace:*",
"@objectstack/objectql": "workspace:*",
"@objectstack/platform-objects": "workspace:*",
"@objectstack/service-automation": "workspace:*",
"@types/node": "^26.6.5",
"typescript": "^6.0.3",
Expand Down
Loading
Loading