Skip to content

Commit f5b0b33

Browse files
authored
feat(mcp): add instructions to the mcp route options (#450)
1 parent 1effbca commit f5b0b33

15 files changed

Lines changed: 157 additions & 2 deletions

File tree

‎docs/content/1.guide/18.hub-initiate.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Registrations are validated fail-fast: one module per type (`DF8108`), an existi
8989

9090
The hub's **single Auth** is one gate at the shared transport for every mounted devframe, built-ins, and the MCP route; one handshake (OTP, magic link, or pre-shared token) unlocks the namespace; `auth: false` disables it for localhost.
9191

92-
The aggregate MCP route mounts through the `'auto'` default once any mounted devframe (or an agent-flagged hub command) exposes agent tools; `mcp: true` forces it on, `mcp: false` off. It has its own origin gate, independent of this RPC Auth: the mounted route trusts same-machine callers, and `mcp: { authorization }` adds an identity check when the hub is reachable beyond loopback. `mcp: { exposeSharedState }` takes `false` or a `(key) => boolean` filter to choose which shared-state keys agents can read through `devframe_state_read` and `devframe://state/<key>`. The hub exposes one aggregate route over every mounted devframe's tools.
92+
The aggregate MCP route mounts through the `'auto'` default once any mounted devframe (or an agent-flagged hub command) exposes agent tools; `mcp: true` forces it on, `mcp: false` off. It has its own origin gate, independent of this RPC Auth: the mounted route trusts same-machine callers, and `mcp: { authorization }` adds an identity check when the hub is reachable beyond loopback. `mcp: { exposeSharedState }` takes `false` or a `(key) => boolean` filter to choose which shared-state keys agents can read through `devframe_state_read` and `devframe://state/<key>`. `mcp: { instructions }` sets the usage guidance that the route sends in the MCP `initialize` result. The hub exposes one aggregate route over every mounted devframe's tools.
9393

9494
## Singular vs hub mounting
9595

‎docs/content/2.adapters/7.mcp.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ devframeViteBridge(myDevframe, {
6969
})
7070
```
7171

72+
### Telling coding agents how to use the tools
73+
74+
`instructions` on the route options is a short text that the MCP server sends in its `initialize` result (and in the `server/discover` result of the 2026-07-28 revision). An MCP client can add it to the context of the coding agent. Use it for guidance that applies to every tool, for example which tool to call first. Without `instructions`, the handshake carries none.
75+
76+
```ts
77+
devframeViteBridge(myDevframe, {
78+
mcp: { instructions: 'Call my-tool_list-sessions first, then pass a session id to the other tools.' },
79+
})
80+
```
81+
82+
`createMcpServer` and `createMcpFetchHandler` take the same `instructions` option.
83+
7284
### Hosted bridges
7385

7486
Both bridges forward the setting to their side-car dev server, advertising the mounted endpoint in `__connection.json`:

‎packages/agentic/src/mcp/__tests__/mcp-server.test.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ function nullHost(): DevframeHost {
1313
}
1414
}
1515

16-
async function bootPair(exposeSharedState: boolean | ((key: string) => boolean) = true) {
16+
async function bootPair(exposeSharedState: boolean | ((key: string) => boolean) = true, instructions?: string) {
1717
const ctx = await createHostContext({ cwd: process.cwd(), mode: 'dev', host: nullHost() })
1818

1919
const server = buildMcpServerFromContext(ctx, {
2020
serverName: 'test',
2121
serverVersion: '0.0.0-test',
2222
exposeSharedState,
23+
instructions,
2324
})
2425

2526
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
@@ -39,6 +40,26 @@ async function bootPair(exposeSharedState: boolean | ((key: string) => boolean)
3940
}
4041

4142
describe('mcp adapter (in-memory)', () => {
43+
it('sends the server instructions in the handshake', async () => {
44+
const { client, cleanup } = await bootPair(true, 'Call greet first.')
45+
try {
46+
expect(client.getInstructions()).toBe('Call greet first.')
47+
}
48+
finally {
49+
await cleanup()
50+
}
51+
})
52+
53+
it('sends no instructions when none are set', async () => {
54+
const { client, cleanup } = await bootPair()
55+
try {
56+
expect(client.getInstructions()).toBeUndefined()
57+
}
58+
finally {
59+
await cleanup()
60+
}
61+
})
62+
4263
it('lists tools registered via ctx.agent.registerTool', async () => {
4364
const { ctx, client, cleanup } = await bootPair()
4465
try {

‎packages/agentic/src/mcp/build-server.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface BuildMcpServerOptions {
1515
serverName: string
1616
serverVersion: string
1717
exposeSharedState: boolean | ((k: string) => boolean)
18+
/** Sent as `instructions` in the `initialize` / `server/discover` result. */
19+
instructions?: string
1820
}
1921

2022
/**
@@ -43,6 +45,7 @@ export function buildMcpServerFromContext(
4345
tools: { listChanged: true },
4446
resources: { listChanged: true },
4547
},
48+
...(options.instructions ? { instructions: options.instructions } : {}),
4649
},
4750
)
4851

@@ -121,6 +124,7 @@ export async function createMcpServer(
121124
serverName: options.serverName ?? `${definition.id} (devframe)`,
122125
serverVersion: options.serverVersion ?? definition.version ?? '0.0.0',
123126
exposeSharedState: options.exposeSharedState ?? true,
127+
instructions: options.instructions,
124128
}
125129

126130
// `serveStdio` pins ONE instance for the connection's lifetime. Each pinned

‎packages/agentic/src/mcp/fetch.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export function createMcpFetchHandler(
7777
serverName: options.serverName,
7878
serverVersion: options.serverVersion,
7979
exposeSharedState: options.exposeSharedState,
80+
instructions: options.instructions,
8081
}))
8182

8283
// A single, long-lived bridge from devframe's change events onto the

‎packages/devframe/src/adapters/__tests__/initiate.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,39 @@ describe('adapters/handler', () => {
360360
}
361361
})
362362

363+
it('mcp: instructions reach the client in the initialize result', async () => {
364+
const wsPort = await getPort({ port: 18145, host: '127.0.0.1' })
365+
const devtools = initDevframe(defineTestDef('handler-mcp-instructions'), {
366+
base: '/__handler-mcp-instructions/',
367+
auth: false,
368+
mcp: { instructions: 'Call list-pages first.' },
369+
ws: { port: wsPort },
370+
})
371+
372+
try {
373+
await devtools.ready
374+
const res = await devtools.handler(new Request('http://localhost:3000/__handler-mcp-instructions/__mcp', {
375+
method: 'POST',
376+
headers: {
377+
'content-type': 'application/json',
378+
'accept': 'application/json, text/event-stream',
379+
'origin': 'http://localhost:3000',
380+
},
381+
body: JSON.stringify({
382+
jsonrpc: '2.0',
383+
id: 1,
384+
method: 'initialize',
385+
params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'test', version: '0.0.0' } },
386+
}),
387+
}))
388+
expect(res.status).toBe(200)
389+
expect(await res.text()).toContain('"instructions":"Call list-pages first."')
390+
}
391+
finally {
392+
await devtools.close()
393+
}
394+
})
395+
363396
// The `'auto'` default: an omitted `mcp` mounts the route exactly when
364397
// `setup()` left a non-empty agent surface.
365398
function defineAgentTestDef(id: string) {

‎packages/devframe/src/adapters/_shared.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export interface ResolvedMcpConfig {
7070
allowedOrigins?: readonly string[] | false
7171
/** Shared-state exposure from the route options; omitted means every key. */
7272
exposeSharedState?: boolean | ((key: string) => boolean)
73+
/** Handshake `instructions` from the route options; omitted means none. */
74+
instructions?: string
7375
/** The resolved identity policy: a bearer token, callback, or `false`. */
7476
authorization: McpAuthorization
7577
}
@@ -99,6 +101,7 @@ export function resolveMcpConfig(mcp: McpSetting | undefined): ResolvedMcpConfig
99101
...(mcp.path !== undefined ? { path: mcp.path } : {}),
100102
...(mcp.allowedOrigins !== undefined ? { allowedOrigins: mcp.allowedOrigins } : {}),
101103
...(mcp.exposeSharedState !== undefined ? { exposeSharedState: mcp.exposeSharedState } : {}),
104+
...(mcp.instructions !== undefined ? { instructions: mcp.instructions } : {}),
102105
authorization,
103106
}
104107
}

‎packages/devframe/src/adapters/initiate.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ async function mountMcpRoute(
380380
serverName: `${def.id} (devframe)`,
381381
serverVersion: def.version ?? '0.0.0',
382382
exposeSharedState: config.exposeSharedState ?? true,
383+
instructions: config.instructions,
383384
authorization: config.authorization,
384385
allowedOrigins: config.allowedOrigins,
385386
})

‎packages/devframe/src/types/devframe.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ export interface McpRouteOptions {
174174
* The states themselves stay available to the UI over RPC.
175175
*/
176176
exposeSharedState?: boolean | ((key: string) => boolean)
177+
/**
178+
* Usage guidance for the route, sent as `instructions` in the MCP
179+
* `initialize` (and `server/discover`) result. An MCP client can add it to
180+
* the coding agent's context, for example to say which tool to call first.
181+
* Omitted or empty sends no instructions.
182+
*/
183+
instructions?: string
177184
}
178185

179186
export interface DevframeCliOptions {

‎packages/devframe/src/types/mcp.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export interface CreateMcpServerOptions {
2525
* - `(key) => boolean`: filter
2626
*/
2727
exposeSharedState?: boolean | ((key: string) => boolean)
28+
/**
29+
* Usage guidance sent as `instructions` in the MCP `initialize` (and
30+
* `server/discover`) result. Omitted or empty sends no instructions.
31+
*/
32+
instructions?: string
2833
/** Override the name reported in the MCP handshake. */
2934
serverName?: string
3035
/** Override the version reported in the MCP handshake. Defaults to `definition.version ?? '0.0.0'`. */
@@ -44,6 +49,8 @@ export interface CreateMcpFetchHandlerOptions {
4449
serverVersion: string
4550
/** Expose shared-state keys as MCP resources; see {@link CreateMcpServerOptions.exposeSharedState}. */
4651
exposeSharedState: boolean | ((key: string) => boolean)
52+
/** Usage guidance for the handshake; see {@link CreateMcpServerOptions.instructions}. */
53+
instructions?: string
4754
/**
4855
* Optional identity check, layered on top of the origin gate and checked
4956
* **after** it: a bearer token string (matched in constant time against

0 commit comments

Comments
 (0)