Skip to content

Commit 778184e

Browse files
committed
feat(mcp): add instructions to the mcp route options
The MCP initialize result can carry instructions that an MCP client adds to the context of the coding agent. The mcp route options, createMcpServer, createMcpFetchHandler and the Next.js host now take instructions, and both mounts (initDevframe and initHub) send them in the initialize result. Omitted or empty instructions send nothing.
1 parent 3c85f76 commit 778184e

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)