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 56baf78
Browse filesBrowse the repository at this point in the historyBrowse files
feat(mcp): per-request server instructions on McpAuthoringInjection, answered only where the authoring family registers (#31141)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiUfC8G7ZWNaCdELoQcXkaCC)_Fixes#31022
Clause-②: yes
Metadata-protocol: none
## 维护者速读
- **改了什么**:MCP
宿主现在可以在「搭建入口」的注入里按请求带一段说明(`McpAuthoringInjection.instructions`),客户端连上来握手时拿到的就是这段说明,而不是那句固定默认话。
- **为什么改**:cloud 的搭建入口要告诉 Claude
等客户端「规则在哪、有哪些」,原来平台只能给全服务器一段固定说明,没法按请求区分;cloud#3658 等这个口子接线。
- **风险与回滚**:只读权限(`data:read`)、API
key、会话等拿不到搭建工具的请求,永远看不到这段说明,仍是原来的默认话;不传这个字段时行为与今天完全一样。回滚即 revert 本 PR。
-
**先看**:`packages/mcp/src/mcp-server-runtime.ts`(`authoringInstructions`
与 `handleHttpRequest` 里 `instructions:`
那一行)、`packages/mcp/src/types.ts`(新字段的注释)、`packages/mcp/src/mcp-server-runtime.authoring-scope.test.ts`(末尾新加的
describe)。
- **两分钟验证**:`pnpm --filter @objectstack/mcp exec vitest run
src/mcp-server-runtime.authoring-scope.test.ts`,预期 24 个用例全绿,其中 5
个是本次新增的说明用例。
- **需要你做的**:无,席位落地。
## What changes
- `McpAuthoringInjection` (`packages/mcp/src/types.ts`) gains an
optional `instructions?: string`.
- `MCPServerRuntime.handleHttpRequest` builds the per-request server
with `authoringInstructions(opts.authoring, grantedScopes) ??
this.config.instructions ?? DEFAULT_SENTENCE` (the hard-coded default).
The new private helper returns the injected text only when
`grantsAuthoringTools(grantedScopes)` admits the request, the same
predicate `registerAuthoringTools` uses to register the family. Every
other request (a `data:read`, `data:write` or `actions:execute` token,
an empty grant, the undefined grant of an API key or a session) gets the
configured or default instructions.
- **Why on the injection, not on `handleHttpRequest`'s opts:** an
opts-level field would be a second, ungated input that any request could
carry, so keeping the `data:read` guarantee would need a second gate
beside the registry's. On the injection, the text sits inside the object
only the authoring family reads, and the one predicate that admits the
registry also admits the text. A host cannot hand it to a request the
family does not serve.
- Changeset: `@objectstack/mcp` patch (Metadata-protocol `none`).
## Tests
A new top-level `describe` in the existing shared harness
`packages/mcp/src/mcp-server-runtime.authoring-scope.test.ts`. Each case
reads `result.instructions` off a real `initialize` round-trip through
`handleHttpRequest`:
1. A `metadata:write` request whose injection carries instructions is
answered with them. A control leg (same door, no field) answers
something else.
2. A `data:read` request with the same injection gets the default,
byte-equal to a `data:read` request with no injection.
3. The undefined grant (API key or session) and a `data:write` token get
the default too.
4. A `metadata:write` request whose injection carries no instructions
gets the default.
5. A runtime configured with server-wide `instructions` keeps them for
every request the injection does not reach. Under the admitting grant,
the per-request text wins.
The default sentence is not pinned verbatim. Each "gets the default"
assertion compares against a control request on the same runtime.
**Ablation** (one behaviour, the grant gate):
`scripts/ablation-replace.mjs` replaced `if (!authoring ||
!grantsAuthoringTools(grantedScopes)) return undefined;` with `if
(!authoring) return undefined;`. Anchor 1→0, blob `f68539a8ec22 →
83181ec01751`. The file went **3 failed / 21 passed (24)**, failing
cases 2, 3 and 5 above. The restore put the blob back to HEAD and `git
diff HEAD` was empty. The test imports `./mcp-server-runtime.js` from
source, so no dist rebuild was involved.
## Acceptance notes
- When the grant admits the family, the injected text is served even if
the host's registry holds no tools. The gate is the grant predicate, as
for registration itself. What the registry holds and what the
instructions say are the host's business.
- An empty-string `instructions` is passed through as given, and the SDK
then omits `instructions` from the answer. Only `undefined` falls back
to the configured or default text.
- The long-lived stdio server (`MCPServerRuntime` constructor) is
untouched. It has no per-request authoring injection.
## Verification
All readings were taken on head `e5540c8f`. `git merge origin/main` was
a no-op: `origin/main` is still the branch point `5f003744`, read via
`ls-remote`.
<details><summary>Package tests, typecheck, lint
(@objectstack/mcp)</summary>
```
pnpm --filter '@objectstack/mcp^...' build :: exit 0 (lock VERDICT command-exit 0)
vitest run src/mcp-server-runtime.authoring-scope.test.ts :: exit 0 Tests 24 passed (24)
pnpm --filter @objectstack/mcp test :: exit 0 Test Files 60 passed (60) · Tests 767 passed (767)
pnpm --filter @objectstack/mcp typecheck :: exit 0 check:test-typecheck OK (debt ledger held)
eslint packages/mcp --no-inline-config (repo `pnpm lint` scoped) :: exit 0
```
`@objectstack/mcp` has no `lint` script of its own, so the repo `pnpm
lint` command (`eslint . --no-inline-config`) was run scoped to
`packages/mcp`.
</details>
<details><summary>Derived gate families (dispatch-gates --commands): 60
derived, 60 run, 0 NOT-MEASURED</summary>
All 60 commands exited 0. Four exited 3 on the first pass and were
re-run green:
- `check:dual-build-cjs-loads` and `check:lean-entry-closure` had no
`dist/`. After a full `pnpm build --concurrency=2` (exit 0), both
returned exit 0.
- `check:type-check-debt` hit a lock queue-timeout. Re-run under the
lock, exit 0.
- `check-plugin-teardown-shape --self-test` could not reach its pinned
fixture commit in the shallow clone. Once that object was present, exit
0.
`dispatch-gates --ran` with exit codes recorded: `60 derived famil(ies)
accounted for — 60 run, 0 NOT-MEASURED (a DERIVED zero)`.
</details>
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(mcp): a host passes per-request server instructions on `McpAuthoringInjection`, answered at `initialize` only where the authoring family registers
6
+
7
+
Clause-②: yes
8
+
Metadata-protocol: none
9
+
10
+
`McpAuthoringInjection` gains an optional `instructions` string. `MCPServerRuntime.handleHttpRequest` answers `initialize` with it when the request's grant registers the injected authoring family (a scope list naming `metadata:write`, the same predicate that admits the registry). Every other request — a `data:read`, `data:write` or `actions:execute` token, an API key or a session — is answered with the runtime's configured `instructions`, or the default sentence, and never sees the injected text; an authoring request whose injection carries no `instructions` gets the configured or default text too. A host that serves a build door can now tell the MCP client where its authoring rules live without widening what a read-only connection is told. Nothing existing changes: a host that passes no `instructions` answers exactly as before.
0 commit comments