Skip to content

Commit a126fc6

Browse files
authored
feat(agent)!: agent-native MCP wave — bridges, core surface, instance registry, devframe connect (#145)
1 parent 7962b8d commit a126fc6

86 files changed

Lines changed: 3321 additions & 593 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ temp
1818
packages/devframe/skills
1919
test-results
2020
playwright-report
21+
tests/e2e/.registries
2122
playwright/.cache
2223
blob-report
2324
.ecosystem
2425
storybook-static
26+
27+
# Agent skills from npm packages (managed by skills-npm)
28+
**/skills/npm-*

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ Framework-neutral foundation for building devframes.
3030
</a>
3131
</p>
3232

33+
## Credits
34+
35+
The `devframe connect` MCP connector (discovery + gateway tools + agent-steering errors) follows the architecture Vercel's [`next-devtools-mcp`](https://github.com/vercel/next-devtools-mcp) validated: the real MCP endpoint lives inside the framework, and a thin external connector discovers and proxies it.
36+
3337
## License
3438

3539
[MIT](./LICENSE.md) License © [Anthony Fu](https://github.com/antfu)

‎alias.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const alias = {
1818
'devframe/node/hub-internals': r('devframe/src/node/hub-internals/index.ts'),
1919
'devframe/node': r('devframe/src/node/index.ts'),
2020
'devframe/constants': r('devframe/src/constants.ts'),
21+
'devframe/utils/agent-tool-name': r('devframe/src/utils/agent-tool-name.ts'),
2122
'devframe/utils/colors': r('devframe/src/utils/colors.ts'),
2223
'devframe/utils/crypto-token': r('devframe/src/utils/crypto-token.ts'),
2324
'devframe/utils/events': r('devframe/src/utils/events.ts'),

‎docs/adapters/mcp.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,50 @@ defineDevframe({
4646
})
4747
```
4848

49+
### Hosted bridges
50+
51+
Both hosted bridges forward the same option to their side-car dev server and advertise the endpoint (with its port) in the `__connection.json` they serve:
52+
53+
```ts
54+
// Vite
55+
viteDevBridge(devframe, { devMiddleware: true, mcp: true })
56+
57+
// Next.js (@devframes/next)
58+
createDevframeNextHandler(devframe, { mcp: true })
59+
```
60+
61+
## Custom hosts
62+
63+
`createMcpFetchHandler(ctx, options)` returns the endpoint as a web-standard `Request → Response` handler plus a `dispose()` for session teardown — mount it on any fetch-shaped server (a Next.js App Router route, a custom Node server). The h3 `mountMcpHttp` used by the dev server is a thin wrapper over it.
64+
65+
```ts
66+
import { createMcpFetchHandler } from 'devframe/adapters/mcp'
67+
68+
const mcp = createMcpFetchHandler(ctx, {
69+
serverName: 'my-tool (devframe)',
70+
serverVersion: '1.0.0',
71+
exposeSharedState: true,
72+
})
73+
// route every method on /__mcp to mcp.fetch(request)
74+
```
75+
76+
## Discovery: `devframe connect`
77+
78+
The `devframe` bin ships an MCP **connector** — a thin discovery + proxy server in the shape [next-devtools-mcp](https://github.com/vercel/next-devtools-mcp) validated. Configure it once in an agent client and it finds every running devframe:
79+
80+
```json
81+
{
82+
"mcpServers": {
83+
"devframe": { "command": "npx", "args": ["devframe", "connect"] }
84+
}
85+
}
86+
```
87+
88+
It exposes two gateway tools (the wire names of the `devframe:connect:*` ids — see [tool ids and wire names](/guide/agent-native#tool-ids-and-wire-names)):
89+
90+
- **`devframe_connect_list-instances`** — discover running devframe dev servers and list each one's MCP tools. Instances running without an MCP route are listed with a hint to restart with `--mcp`.
91+
- **`devframe_connect_call-tool`** — invoke one tool on one instance (`{ port, tool, args }`) over its Streamable-HTTP endpoint.
92+
93+
Discovery reads the **instance registry**: every `createDevServer` (CLI `dev`, `viteDevBridge`, `@devframes/next`'s handler) writes a record to `~/.devframe/instances/<pid>-<port>.json` on boot and removes it on close; readers prune records whose liveness probe fails. In-process hosts register explicitly with `registerDevframeInstance` from `devframe/node` — see `createDevframeNextHost().mountMcp` for serving MCP on a Next app's own origin. `--port <n>` probes an explicit port besides the registry; `DEVFRAME_INSTANCES_DIR` relocates the registry and `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` opts a server out.
94+
4995
See the [Agent-Native](/guide/agent-native) page for the full API, safety model, and Claude Desktop integration example.

‎docs/errors/DF0045.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0045: Instance Registry Update Failed
6+
7+
## Message
8+
9+
> Failed to update the devframe instance registry at "`{file}`": `{reason}`
10+
11+
## Cause
12+
13+
A dev server (or an in-process host calling `registerDevframeInstance`) could not write or remove its record under the instance registry directory — `~/.devframe/instances/` by default, or `$DEVFRAME_INSTANCES_DIR`. Typical causes are a read-only home directory, missing permissions, or a full disk. The server keeps running; only discovery is affected — `devframe connect` will not see this instance.
14+
15+
## Fix
16+
17+
- Check that the registry directory is writable and the disk has free space.
18+
- Point `DEVFRAME_INSTANCES_DIR` at a writable directory.
19+
- Set `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` to opt out of registration entirely.
20+
21+
## Source
22+
23+
- [`packages/devframe/src/node/instance-registry.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/instance-registry.ts)`registerDevframeInstance()` reports this on a failed write and its `unregister()` on a failed removal.

‎docs/errors/DF0046.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0046: Connector Requires the MCP SDK
6+
7+
## Message
8+
9+
> `devframe connect` requires the optional peer dependency @modelcontextprotocol/server: `{reason}`
10+
11+
## Cause
12+
13+
`devframe connect` was started but `@modelcontextprotocol/server` could not be imported. The SDK is an optional peer dependency of `devframe` — the MCP surface stays opt-in, so it only needs to be installed where MCP features are used.
14+
15+
## Fix
16+
17+
Install the SDK next to devframe and run the connector again:
18+
19+
```sh
20+
npm install @modelcontextprotocol/server
21+
devframe connect
22+
```
23+
24+
## Source
25+
26+
- [`packages/devframe/src/cli/connect.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/cli/connect.ts)`startConnectServer()` throws this when the dynamic SDK import fails.

‎docs/errors/DF0047.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0047: Agent Tool Wire-Name Collision
6+
7+
## Message
8+
9+
> Agent tool "`{id}`" is hidden from the MCP surface: its wire name "`{name}`" collides with the tool "`{existing}`".
10+
11+
## Cause
12+
13+
MCP clients constrain tool names to `^[a-zA-Z0-9_-]{1,128}$`, so the MCP adapter derives each tool's wire name from its id (runs of characters outside `[a-zA-Z0-9_-]` become a single `_`). Two registered ids sanitized to the same wire name — e.g. `demo:greet` and `demo_greet`. The first registration keeps the name; the later tool is hidden from `tools/list`.
14+
15+
## Example
16+
17+
```ts
18+
ctx.agent.registerTool({ id: 'demo:greet', description: '', handler })
19+
ctx.agent.registerTool({ id: 'demo_greet', description: '', handler }) // hidden: same wire name
20+
```
21+
22+
## Fix
23+
24+
Rename one of the two ids so they sanitize to distinct wire names. Namespaced ids (`devframes:plugin:<slug>:<fn>`, `devframe:<area>:<fn>`) collide only when they differ solely in separator characters.
25+
26+
## Source
27+
28+
- [`packages/devframe/src/adapters/mcp/build-server.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/mcp/build-server.ts) — the `tools/list` handler reports this once per hidden tool when deduplicating wire names.

‎docs/errors/DF0048.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0048: Unknown Shared-State Key
6+
7+
## Message
8+
9+
> Unknown shared-state key "`{key}`".
10+
11+
## Cause
12+
13+
The built-in `devframe_state_read` MCP tool was called with a `key` that is not among the shared-state keys the host publishes (or that the `exposeSharedState` filter allows). The error crosses the MCP boundary as structured JSON, so the calling agent can self-correct.
14+
15+
## Example
16+
17+
```ts
18+
// The host publishes only `my-plugin:counter`; an agent calls:
19+
// devframe_state_read({ key: 'my-plugin:cuonter' }) → DF0048
20+
```
21+
22+
## Fix
23+
24+
Call the `devframe_state_read` tool without arguments to list the available keys, then retry with one of them.
25+
26+
## Source
27+
28+
- [`packages/devframe/src/adapters/mcp/build-server.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/mcp/build-server.ts)`readStateResult()` throws this when the requested key is absent from the filtered key list.

‎docs/errors/DF0049.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0049: Connector Call Requires Port and Tool
6+
7+
## Message
8+
9+
> The devframe_connect_call-tool tool requires { port: number, tool: string }.
10+
11+
## Cause
12+
13+
The `devframe connect` gateway tool `devframe_connect_call-tool` was invoked without a numeric `port` or a string `tool` name — the two fields that identify which instance to dial and which of its tools to call. The error crosses the MCP boundary as structured JSON, so the calling agent can self-correct.
14+
15+
## Example
16+
17+
```ts
18+
// devframe_connect_call-tool({ tool: 'devframe_state_read' }) → DF0049 (missing port)
19+
```
20+
21+
## Fix
22+
23+
Call `devframe_connect_list-instances` first — its result carries each instance's `port` and tool names — then retry with both fields.
24+
25+
## Source
26+
27+
- [`packages/devframe/src/cli/connect.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/cli/connect.ts)`call()` throws this when the gateway arguments fail validation.

‎docs/errors/DF0050.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0050: No Devframe Instance on Port
6+
7+
## Message
8+
9+
> No running devframe instance on port `{port}`.
10+
11+
## Cause
12+
13+
The `devframe connect` gateway tool `devframe_connect_call-tool` targeted a port with no live devframe instance behind it — neither the instance registry nor a direct probe of the port found one serving `__connection.json`. The instance may have stopped, restarted on a different port, or never existed. The error crosses the MCP boundary as structured JSON, so the calling agent can self-correct.
14+
15+
## Example
16+
17+
```ts
18+
// No dev server on 5199:
19+
// devframe_connect_call-tool({ port: 5199, tool: 'devframe_state_read' }) → DF0050
20+
```
21+
22+
## Fix
23+
24+
Call `devframe_connect_list-instances` for the current instance list and retry with a live port.
25+
26+
## Source
27+
28+
- [`packages/devframe/src/cli/connect.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/cli/connect.ts)`call()` throws this when neither the registry nor the port probe finds an instance.

0 commit comments

Comments
 (0)