Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 93 additions & 2 deletions docs/content/1.guide/15.agent-native.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,32 @@ const handle = ctx.agent.registerToolProvider(() =>
handle.notifyChanged() // fires tools/list_changed
```

## Reporting tool progress

Registered and provider tool handlers receive an optional request-bound invocation context:

```ts
ctx.agent.registerTool({
id: 'my-plugin:build',
description: 'Build the current project.',
handler: async (_args, invocation) => {
await invocation?.reportProgress({
progress: 1,
total: 2,
message: 'Compiling',
})
await compileProject()
await invocation?.reportProgress({
progress: 2,
total: 2,
message: 'Complete',
})
},
})
```

`progress` and `total` are finite numbers, and `progress` increases with every report in one invocation. Reports are delivered in order while the handler is running. MCP callers that provide a progress token receive `notifications/progress`; the reporter is a no-op for callers without one. Agent-enabled RPC functions retain their existing handler signatures, so request-bound progress belongs on registered and provider tools.

## Registering a resource

Readable snapshots by URI:
Expand All @@ -96,7 +122,72 @@ ctx.agent.registerResource({
})
```

Every `ctx.rpc.sharedState` key is exposed as a `devframe://state/<key>` resource and via the **`devframe:state:read` tool** (wire `devframe_state_read`): no args → key list, `key` → its value. `exposeSharedState: false` (or a filter) on `createMcpServer` opts out.
Devframe assigns `devframe://resource/<encoded-id>` by default. Set `uri` to expose another URI. `read` runs for every MCP read and may receive the requested `URL`.

## Registering resource templates

Templates describe resources whose URI contains variables. Devframe uses the MCP SDK's URI-template parser and passes the parsed variables to `read`.

```ts
const logsResource = ctx.agent.registerResource({
id: 'process-logs',
uriTemplate: 'rolldown://logs/{process}',
name: 'Process logs',
mimeType: 'text/plain',
list: () => ({
resources: runningProcesses().map(process => ({
uri: `rolldown://logs/${encodeURIComponent(process.name)}`,
name: `${process.name} logs`,
mimeType: 'text/plain',
})),
}),
read: (_uri, variables) => ({
text: readLogs(String(variables.process)),
}),
})

logsResource.notifyUpdated('rolldown://logs/worker')
```

MCP exposes templates through `resources/templates/list`. When `list` is present, its concrete entries also appear in `resources/list`.

## Publishing resource updates

Resource handles publish invalidations after their underlying value changes:

```ts
const buildResource = ctx.agent.registerResource({
id: 'live-build',
name: 'Live build',
read: () => ({ json: currentBuild() }),
})

buildEvents.on('changed', () => buildResource.notifyUpdated())
```

`notifyUpdated()` sends no content. MCP 2026 callers receive the invalidation through `subscriptions/listen` when its `resourceSubscriptions` filter contains the URI, then call `resources/read` for the current value. Legacy MCP callers use `resources/list`, `resources/templates/list`, and `resources/read` to pull current values.

## Deriving resources from other state

Resource providers are queried when Devframe lists, resolves, or reads resources. Use them when another registry already owns the definitions.

```ts
const resources = ctx.agent.registerResourceProvider(() =>
currentDatasets().map(dataset => ({
id: `dataset:${dataset.id}`,
uri: `dataset://${dataset.id}`,
name: dataset.name,
read: () => ({ json: dataset.snapshot() }),
})),
)

resources.notifyChanged() // resources/list_changed
resources.notifyUpdated('dataset://builds/active') // resources/updated through MCP 2026 subscriptions/listen
```

Direct registrations win over providers. Earlier providers win over later providers, and exact resource URIs win over templates.

Every `ctx.rpc.sharedState` key is exposed as a `devframe://state/<encoded-key>` resource and via the **`devframe:state:read` tool** (wire `devframe_state_read`): no args → key list, `key` → its value. MCP 2026 subscriptions receive shared-state invalidations under the same encoded URI. `exposeSharedState: false` (or a filter) on `createMcpServer` opts out.

## Starting the MCP server

Expand Down Expand Up @@ -135,7 +226,7 @@ In `claude_desktop_config.json`:
}
```

Restart; tools appear in the drawer, resources as `devframe://resource/<id>` / `devframe://state/<key>` URIs.
Restart; tools appear in the drawer. Resources use their declared URI, the generated `devframe://resource/<id>` URI, or `devframe://state/<key>` for implicit shared state.

## Writing descriptions agents act on

Expand Down
34 changes: 34 additions & 0 deletions docs/content/6.errors/DF0071.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: 'DF0071: Invalid Agent Tool Progress'
description: 'Invalid agent tool progress: {reason}.'
---

## Message
> Invalid agent tool progress: `{reason}`.

## Cause
A registered or provider tool reported a non-finite `progress` or `total`, or reported `progress` that did not increase from its previous value in the same invocation.

## Example

```ts
handler: async (_args, invocation) => {
await invocation?.reportProgress({ progress: 1, total: 2 })
await invocation?.reportProgress({ progress: 1, total: 2 })
}
```

## Fix

Use finite numbers and increase `progress` on every report within one tool invocation.

```ts
handler: async (_args, invocation) => {
await invocation?.reportProgress({ progress: 1, total: 2 })
await invocation?.reportProgress({ progress: 2, total: 2 })
}
```

## Source

- [`packages/devframe/src/node/host-agent.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-agent.ts) - `invokeToolHandler()` validates progress before delivering it.
1 change: 1 addition & 0 deletions docs/content/6.errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Emitted by `devframe` — the framework-neutral host, RPC, streaming, assets, se
| [DF0068](/errors/DF0068) | error | Required Service Version Range Not Satisfied |
| [DF0069](/errors/DF0069) | warn | Service Version Range Not Satisfied |
| [DF0070](/errors/DF0070) | error | Invalid Service |
| [DF0071](/errors/DF0071) | error | Invalid Agent Tool Progress |
| [DF0072](/errors/DF0072) | warn | Snapshot Names Unknown RPC Method |
| [DF0073](/errors/DF0073) | error | JSON-Render Spec Does Not Match Its Schema |
| [DF0074](/errors/DF0074) | error | JSON-Render Schema Is Asynchronous |
Expand Down
3 changes: 2 additions & 1 deletion docs/content/8.references/3.events.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ Emitted on `ctx.agent.events`; adapters (e.g. the MCP server) re-publish their m
|---|---|---|
| `agent:manifest:changed` | any tool/resource/provider change | — |
| `agent:tool:registered` / `agent:tool:unregistered` | `registerTool` / `unregisterTool` | `AgentTool` / id |
| `agent:resource:registered` / `agent:resource:unregistered` | `registerResource` / `unregisterResource` | `AgentResource` / id |
| `agent:resource:registered` / `agent:resource:unregistered` | `registerResource` / `unregisterResource` | `AgentResource` or `AgentResourceTemplate` / id |
| `agent:resource:updated` | resource or provider handle `notifyUpdated` | concrete URI |

### RPC client connection events

Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/adapters/initiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export function initDevframe(
const mounted = mountMcpHttp(app, context, mcpPath, {
serverName: `${def.id} (devframe)`,
serverVersion: def.version ?? '0.0.0',
exposeSharedState: true,
exposeSharedState: mcpConfig.exposeSharedState ?? true,
allowedOrigins: mcpConfig.allowedOrigins,
})
mcpDispose = mounted.dispose
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { DevframeDefinition } from '../../../../types/devframe'
import { createMcpServer } from '../../build-server'

const definition: DevframeDefinition = {
id: 'progress-stdio-test',
name: 'Progress stdio test',
version: '1.0.0',
packageName: '@devframe/progress-stdio-test',
homepage: 'https://example.com',
description: 'Stdio progress test fixture.',
setup(ctx) {
ctx.agent.registerTool({
id: 'build',
description: 'Build the project.',
handler: async (_args, invocation) => {
await invocation?.reportProgress({ progress: 1, total: 2, message: 'Compiling' })
await invocation?.reportProgress({ progress: 2, total: 2, message: 'Testing' })
return { status: 'complete' }
},
})
},
}

await createMcpServer(definition, { transport: 'stdio' })
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { DevframeDefinition } from '../../../../types/devframe'
import { createMcpServer } from '../../build-server'

const definition: DevframeDefinition = {
id: 'resource-stdio-test',
name: 'Resource stdio test',
version: '1.0.0',
packageName: '@devframe/resource-stdio-test',
homepage: 'https://example.com',
description: 'Stdio resource test fixture.',
async setup(ctx) {
const state = await ctx.rpc.sharedState.get('stdio:counter', {
initialValue: { count: 0 },
})
const fixed = ctx.agent.registerResource({
id: 'status',
uri: 'https://example.com/status',
name: 'Status',
read: uri => ({ json: { uri: uri.toString(), status: 'ok' } }),
})
const ignored = ctx.agent.registerResource({
id: 'ignored',
name: 'Ignored',
read: () => ({ json: { ignored: true } }),
})
ctx.agent.registerTool({
id: 'increment-state',
description: 'Increment the fixture state.',
handler: () => {
state.mutate(value => void (value.count += 1))
fixed.notifyUpdated()
ignored.notifyUpdated()
},
})
ctx.agent.registerResource({
id: 'logs',
uriTemplate: 'devframe://logs/{name}',
name: 'Logs',
list: () => ({ resources: [{ uri: 'devframe://logs/app', name: 'App logs' }] }),
read: (_uri: URL, variables: Readonly<Record<string, string | string[]>>) => ({
json: { process: variables.name },
}),
})
},
}

await createMcpServer(definition, { transport: 'stdio' })
Loading
Loading