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 dd03ee0
Browse filesBrowse the repository at this point in the historyBrowse files
feat(connector-mcp): a stdio MCP server takes its credential from the secrets layer through transport.credentialEnv (#29621)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi95wDB21nUbCmU5szPePATGS)_Fixes#29434
domain:server#3 · os-steve · session_01ADHkmRFhrJgbwLP5mvZkeJ
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #29625 —
skills/objectstack-automation/references/integration-recipes.md recipe 6
still says a stdio server's credentials ride `transport.env`;
`content/docs/automation/connectors.mdx` is updated here.
## 维护者速读
- 改了什么:`mcp` 连接器的 stdio 服务器现在可以从密钥层拿凭据。连接器照常声明 `auth: { type,
credentialRef }`,再在 `providerConfig.transport.credentialEnv`
写上服务器读取的环境变量名,平台解析出的密钥只放进子进程的环境变量。
- 为什么改:以前 stdio 服务器的 API key 只能以明文写在 `transport.env` 里,存在元数据中;#29341
的提示也只能劝作者改用 http。
- 风险与回滚:只是新增一个作者可写的可选键(Metadata-protocol: added,connector-mcp 升
minor),没写 `credentialEnv` 的 stdio 实例行为完全不变;新键写错(变量名非法、用在 http 上、没有
auth、与 env 重名)按配置错误拒绝。回滚即 revert 本 PR。
-
先看:`packages/connectors/connector-mcp/src/mcp-provider.ts`(`credentialEnvOf`、stdio
分支合并
env)、`packages/connectors/connector-mcp/src/mcp-stdio-credential-env.test.ts`、`packages/lint/src/lint-connector-credential-literals.ts`(提示措辞)。
- 两分钟验证:`pnpm --filter @objectstack/connector-mcp exec vitest run
src/mcp-stdio-credential-env.test.ts`,预期 17/17 通过(真实子进程只回报变量「已设置」和长度)。
- 需要你做的:无,席位落地。
## What changes
A declarative `provider: 'mcp'` instance over **stdio** can now take its
credential from the secrets layer instead of a literal in
`transport.env` (route (c), ruled by the seat on the card).
- **Route.** The entry declares `auth: { type: 'bearer' | 'api-key' |
'basic', credentialRef }` exactly as an http instance does. The
automation service resolves the reference through its existing chain
(environment, then the stored connector credential, then the host
resolver). The `mcp` provider then puts the resolved secret (the bearer
token, the api-key key, or the basic password) into the child's
environment under the variable that the new provider-owned key
`providerConfig.transport.credentialEnv` names. `providerConfig` is
provider-owned and untyped in the spec (ADR-0023), so there is no
`packages/spec` hunk.
- **Inherited for free.** All of this keys off `entry.auth`: Setup →
Connector credentials lists the reference, a credential write
re-materializes and re-spawns the instance, and a reference no arm
answers degrades the instance naming the reference. That last case never
calls the provider, so the server is never spawned.
- **Where the value goes.** It only goes into the `env` handed to the
spawn (the SDK's `StdioClientTransport` merges it over its default
inherited set). It never goes onto the connector `def`, into a log, or
into a message. A metadata read serves `auth.credentialRef` and
`transport.credentialEnv`, which are two names; `transport.env` stays
withheld whole, as before.
- **Refusals.** These are configuration faults, thrown plain before
anything is spawned, and none of them names a value:
- a `credentialEnv` that is not an environment-variable name;
- a `credentialEnv` on an http transport;
- a `credentialEnv` with no static `auth` (absent, or `none`);
- a `credentialEnv` naming a variable that `transport.env` also sets.
A grant-bearing instance on stdio still meets the existing
credential-handle refusal. That refusal's sentence now names this route
(before, it pointed at "a static credential for the stdio one", which
reached nothing).
- **Unchanged (ruling Q2 A).** A stdio transport with no `credentialEnv`
still does not apply `auth`. The silent drop is #29607. A stdio
handshake does not count as the credential being sent (`credentialSent`
/ `credentialVerified` stay http-only).
- **Advisory (#29341, `connector-credential-literal`).** The stdio env
finding now steers to the route: "move it to the connector's
`auth.credentialRef` with `transport.credentialEnv`". Its hint uses the
same `auth: { type: 'bearer', credentialRef }` spelling as the header
hint. The matching `os explain` paragraph is updated too.
- **Pending release note corrected.**
`.changeset/29341-connector-credential-literal.md` (the #29341
advisory's changeset, still pending on `main`) said that a stdio MCP
server takes no `auth`, and steered authors to http. It would have
shipped in the same release as this route. Its stdio bullet now names
the `credentialEnv` route; this is a prose-only edit. `Check Changeset`
/ `check-empty-changeset` stays red on that file by design (the
DELIBERATE CORRECTION class). The seat ordered the correction in the
claim amendment on #29434.
- **Docs.** In `content/docs/automation/connectors.mdx`, the stdio
bullet now names `credentialEnv`, there is a new "A stdio server's
credential" section with an `os:check` example, and the grant-bearing
paragraph sentence is updated.
**Forced test edits.**
`packages/lint/src/lint-connector-credential-literals.test.ts` had two
edits:
- the stdio env message pin now names the new route (was: "a stdio
server takes no `auth`; `auth.credentialRef` reaches it only over
http");
- the `os explain` fact `A stdio MCP server takes no \`auth\`` is
replaced by `A stdio MCP server takes the same \`auth\`` and ``
`providerConfig.transport.credentialEnv` ``.
One test was added in that file (the hint names the route with the
shared spelling).
## Tests
`packages/connectors/connector-mcp/src/mcp-stdio-credential-env.test.ts`
(new, 17 tests). Every value in it is an obvious placeholder.
- **Real spawns through the SDK.** The fixture child reports only
whether the variable is set and its length, never the value.
- bearer, api-key and basic each reach the child in the named variable,
and the materialization carries no value;
- control: the same instance with no `credentialEnv` starts the child
without the variable;
- control: a literal in `transport.env` reaches the child as today.
- **Refusals.** Each of the four is thrown plain and names no value, and
nothing is spawned (the not-a-name case is run with both a malformed
string and a non-string). A grant handle on stdio meets the handle
refusal, which now names `credentialEnv`.
- **Metadata read.** The connector redactor serves `auth.credentialRef`
and `transport.credentialEnv`, and `redactedKeys` is exactly
`providerConfig.transport.env`.
- **Through the automation service** (LiteKernel +
AutomationServicePlugin + ConnectorMcpPlugin, a real stdio child):
- a reference resolved by the host reaches the spawned server, and the
served descriptor carries no value;
- an unresolvable reference degrades the instance with `credential '…'
is not set`, and the child's start marker is never written.
- **Advisory.** The control (a literal in `transport.env` still draws
the advisory) and the new wording are pinned in the lint test.
<details><summary>Ablations (each leg through
<code>scripts/ablation-replace.mjs</code>: anchor 1→0, blob moved, then
restored to blob == HEAD with an empty <code>git diff HEAD</code>; the
suites import <code>src</code>, so no dist leg)</summary>
- A1, the env merge dropped (`env: env`): 4 failed (bearer, api-key,
basic, composition-resolved), `expected 'unset:0' to be 'set:28'`.
- A2, the variable-name check dropped: 2 failed (not-a-name,
not-a-string), `expected a throw`.
- A3, the http refusal dropped: 1 failed (`credentialEnv on an http
transport`).
- A4, the no-auth refusal dropped: 2 failed (no auth: `TypeError`
instead of the plain refusal; `none`: no throw).
- A5, the clash refusal dropped: 1 failed (`sets environment variable …
twice`).
- A6, the old advisory steer restored: 1 failed (the stdio message pin,
old vs new sentence).
</details>
## Acceptance notes
- **Declaration and level:** `credentialEnv` is a key a metadata author
writes, so the kind is `added` even with no spec hunk (AGENTS.md
Post-Task Checklist step 3; precedent PR #27387,
`providerConfig.include` on `connector-openapi`).
`@objectstack/connector-mcp` takes `minor`. `@objectstack/lint` changes
only message text and stays `patch`. Both lines follow the seat's claim
amendment on #29434.
- **Review face:** `node scripts/pm/record-recognisers.mjs --template
--paths` on the final file list. Without the changesets it reads the
declaration face, `TIER_DEFAULT`. With them it refuses and defers to the
changeset's `Clause-②: yes (widening)` line, which is the same
declaration face.
- **Tool-list refusals over stdio** stay on the plain path. The seat
confirmed option A: `credentialSent` / `credentialVerified` stay
http-only.
- **One secret per instance.** A server that reads two secrets keeps the
second in `env`, or is declared as two instances. An `api-key`'s
`headerName` / `paramName` are http placement and are not read on stdio.
A `basic` username is not a secret, so it goes in `env` if the server
reads it.
- `protocol.ts` and every ADR are untouched. No ADR declares or rules
out a stdio credential route. ADR-0024 §3 (credentials at the
process/transport boundary) and ADR-0097 §3 (`credentialRef` resolved by
the host) both hold as written.
## Verification
Readings head: `f3ff3349` (merges `origin/main` `81cd9eb0`). Every
reading below was taken on that head. The one commit after it,
`a76ad3f8`, changes only the two changesets' prose and front matter, so
no test, gate or dogfood reading was re-taken. On `a76ad3f8` the
changeset gates ran as follows: `check-adr-0087-registration`,
`check-changeset-no-major`, `check:changeset-gate-self-tests`,
`check:pm-changeset-deadline-census`, `check:issue-citations` and
`check:nul-bytes` all exit 0. `check-empty-changeset` exits 1 on
`.changeset/29341-connector-credential-literal.md`, the declared
deliberate correction above.
<details><summary>Package suites and typecheck</summary>
- `@objectstack/connector-mcp`: Test Files 9/9, Tests 119/119; typecheck
exit 0 (incl. `check:test-typecheck`).
- `@objectstack/lint`: Test Files 192/192, Tests 7892/7892; typecheck
exit 0.
</details>
<details><summary>Full dogfood battery (credentials card)</summary>
- Closure build `turbo run build --filter='@objectstack/dogfood^...'`
(plus the connector-mcp and lint closures) under the lock: `VERDICT
command-exit 0`, 68/68 tasks.
- `pnpm --filter @objectstack/dogfood test` under the lock: `VERDICT
command-exit 0`, Test Files 398 passed and 3 skipped (401), Tests 3280
passed and 28 skipped (3308), 0 failed.
</details>
<details><summary>Derived gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 87 families derived on `f3ff3349`, all 87
run.
- Three first answered exit 3 (PREREQUISITE NOT MET) and were re-run
after their prerequisite, then exited 0:
- `check-plugin-teardown-shape --self-test`, after fetching its pinned
fixture commit;
- `check:skill-examples` and `check:dual-build-cjs-loads`, after `turbo
run build --filter='./packages/**'` (78/78 cache hits).
- `--ran` reconciliation: `87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN`
(a DERIVED zero).
- `pnpm lint` (repo-wide eslint, `--no-inline-config`): exit 0.
- CI families are not re-run locally; the PR check-runs are the record.
</details>
<!-- signed: domain:server#3 · os-steve ·
session_01ADHkmRFhrJgbwLP5mvZkeJ -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: .changeset/29341-connector-credential-literal.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,6 +11,6 @@ Metadata-protocol: none
11
11
A declarative connector entry's four maps — `providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers` and `providerConfig.transport.env` — are withheld whole on the metadata read path, but a literal credential in one of them is still stored in metadata. The same `credential-literal` predicate the `flow-credential-literal` advisory asks now judges those four maps and names each credential-shaped entry by its position (never its value), with the `auth.credentialRef` route:
12
12
13
13
- a header entry: move it to the connector's `auth: { type: 'bearer' | 'api-key' | 'basic', credentialRef }`, which the `rest` provider and an `mcp` http transport send themselves;
14
-
- a stdio `transport.env` entry: a stdio MCP server takes no `auth`, so the warning says `auth.credentialRef` reaches the server only over an http transport.
14
+
- a stdio `transport.env` entry: move it to the connector's `auth: { type: 'bearer' | 'api-key' | 'basic', credentialRef }` and name the variable in `providerConfig.transport.credentialEnv`; the `mcp` provider hands the resolved credential to the stdio server in that variable itself.
15
15
16
16
Always a `warning`; nothing is refused and every save lands. The rule runs on all three CLI commands and on the runtime publish gate for `connector` (`PUT /meta/connector/:name`), where it rides the save response's `advisories`. `os explain connector-credential-literal` carries the reasoning.
`provider: 'mcp'`: a stdio MCP server can now take its credential from the secrets layer instead of a literal in `transport.env`. Declare the instance's `auth` with a `credentialRef` as for an http server, and name the environment variable the server reads in the new `providerConfig.transport.credentialEnv` key: the host resolves the reference (environment, Setup → Connector credentials, the host's resolver) and the provider hands the resolved secret (the `bearer` token, the `api-key` key, or the `basic` password) to the child in that variable. The value is never stored, logged, served on a metadata read or put in a message; a reference that resolves to nothing degrades the instance naming the reference and the server is not started.
Refused at materialization as configuration faults (a failed boot for the host's own entry): a `credentialEnv` that is not an environment-variable name, one on an `http` transport, one with no `bearer` / `api-key` / `basic``auth`, and one naming a variable `transport.env` also sets. A stdio transport that names no `credentialEnv` is unchanged: it does not apply `auth`.
17
+
18
+
`@objectstack/lint`: the `connector-credential-literal` advisory's finding for a credential literal in `transport.env` now steers to this route (`auth.credentialRef` with `transport.credentialEnv`) instead of telling the author to move to an http transport.
0 commit comments