Skip to content

Commit 70df5a3

Browse files
committed
fix(mcp): preserve default backend for cloud spawn
Use the configured default backend when spawn_cloud_agent has no runtime profile backend, and add a SQLite-backed MCP smoke test covering config bootstrap, dispatch, logs, and results.
1 parent 5cdbb1f commit 70df5a3

3 files changed

Lines changed: 100 additions & 14 deletions

File tree

README.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ A single MCP server exposes nine tools to your assistant:
4848

4949
| Tool | What it does |
5050
| --- | --- |
51-
| `spawn_cloud_agent` | The shortcut. One `instruction`, defaults from a runtime profile, returns a durable `task_id` and optional `cloud_agent` metadata. |
51+
| `spawn_cloud_agent` | The shortcut. One `instruction`, defaults from a runtime profile, returns a durable `taskId` and optional `cloudAgent` metadata. |
5252
| `dispatch_task` | The escape hatch. Full control over provider, capability, backend, target, and input. |
5353
| `get_task_status` | Current status for a dispatched task. |
5454
| `get_task_logs` | Paginated logs, cursor-based. |
@@ -62,7 +62,7 @@ The model side looks like this:
6262

6363
```ts
6464
spawn_cloud_agent({ instruction: "Audit our S3 buckets for public read and report findings." })
65-
// → { task_id: "task_...", status: "running", cloud_agent: { ... } }
65+
// → { taskId: "task_...", status: "queued", cloudAgent: { ... } }
6666

6767
get_task_status({ task_id: "task_..." })
6868
get_task_logs({ task_id: "task_...", cursor: 0, limit: 200 })
@@ -75,6 +75,8 @@ All tool inputs are validated with [Zod](https://zod.dev). See [`src/schemas.ts`
7575

7676
`spawn_cloud_agent` is intentionally small. Runtime profiles supply the provider, account, backend, target mode, framework, model, protocol, and default tools.
7777

78+
Tool inputs use MCP-friendly snake_case. Responses return the same camelCase object shape as the AgentDispatch SDK and core runtime.
79+
7880
```json
7981
{
8082
"runtime": "research-agent",
@@ -91,21 +93,22 @@ Immediate response:
9193

9294
```json
9395
{
94-
"task_id": "task_...",
95-
"status": "running",
96+
"taskId": "task_...",
97+
"status": "queued",
9698
"provider": "aws",
97-
"account_profile": "dev-aws",
99+
"accountProfile": "dev-aws",
98100
"capability": "agent-runtime",
99101
"backend": "aws-agentcore",
100102
"poll": {
101-
"status_tool": "get_task_status",
102-
"logs_tool": "get_task_logs",
103-
"result_tool": "get_task_result"
103+
"statusTool": "get_task_status",
104+
"logsTool": "get_task_logs",
105+
"resultTool": "get_task_result"
104106
},
105-
"cloud_agent": {
107+
"cloudAgent": {
106108
"protocol": "a2a",
107109
"provider": "aws",
108110
"backend": "aws-agentcore",
111+
"accountProfile": "dev-aws",
109112
"sessionId": "ad-f7221e93f25499a0a1fc0160f63c7621",
110113
"invocation": {
111114
"type": "aws.agentcore.invoke_agent_runtime",
@@ -203,7 +206,7 @@ The defaults come from your runtime profile. From the model side, `spawn_cloud_a
203206
3. Core picks the capability and adapter, persists the task, dispatches.
204207
4. Adapter invokes the worker in your cloud.
205208
5. Worker runs an agent framework on managed cloud infrastructure.
206-
6. Status, logs, results, and optional `cloud_agent` metadata flow back through the same path.
209+
6. Status, logs, results, and optional `cloudAgent` metadata flow back through the same path.
207210

208211
## Configuration
209212

@@ -261,7 +264,7 @@ npx agentdispatch-mcp --config agentdispatch.config.json --check
261264
`spawn_cloud_agent` is the control-plane call. After spawn, the lead agent can:
262265

263266
- Poll via `get_task_status`, `get_task_logs`, and `get_task_result`.
264-
- Continue native subagent interaction with returned `cloud_agent` protocol metadata.
267+
- Continue native subagent interaction with returned `cloudAgent` protocol metadata.
265268
- Use A2A JSON-RPC `message/send` when the runtime protocol is `a2a`.
266269
- Cancel through AgentDispatch so provider references remain durable.
267270

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,12 @@ function createSpawnCloudAgentRequest(runtime: RuntimeService, input: {
7676
const targetMode = input.target?.mode ?? profile?.target?.mode ?? defaults.targetMode ?? "session";
7777
const protocol = input.protocol ?? input.target?.protocol ?? profile?.protocol ?? profile?.target?.protocol ?? defaults.protocol;
7878
const capability = selectCapability(runtime, account.provider, targetMode, profile?.capability ?? defaults.capability);
79+
const backend = profile?.backend ?? defaults.backend;
7980
return {
8081
provider: account.provider,
8182
accountProfile: account.name,
8283
capability,
83-
backend: profile?.backend,
84+
backend,
8485
taskType: "agent.run",
8586
target: {
8687
mode: targetMode,

test/e2e.test.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
22
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3-
import { describe, expect, it } from "vitest";
3+
import { mkdtemp, rm } from "node:fs/promises";
4+
import { join } from "node:path";
5+
import { tmpdir } from "node:os";
6+
import { afterEach, describe, expect, it } from "vitest";
47
import { RuntimeService, type BackendAdapter, type DispatchRequest, type RuntimeEvent, type RuntimeRecord, type TaskStore } from "@agent-dispatch/core";
5-
import { createAgentDispatchMcpServer } from "../src/index.js";
8+
import { createAgentDispatchMcpServer, createRuntimeServiceFromConfig } from "../src/index.js";
9+
10+
const tempDirs: string[] = [];
11+
12+
afterEach(async () => {
13+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
14+
});
615

716
class MemoryStore implements TaskStore {
817
private readonly tasks = new Map<string, any>();
@@ -252,4 +261,77 @@ describe("MCP tool invocation", () => {
252261
await server.close();
253262
}
254263
});
264+
265+
it("runs spawn_cloud_agent end-to-end through config bootstrap and SQLite state", async () => {
266+
const stateDir = await mkdtemp(join(tmpdir(), "agentdispatch-mcp-e2e-"));
267+
tempDirs.push(stateDir);
268+
const runtime = await createRuntimeServiceFromConfig({
269+
stateDir,
270+
accounts: {
271+
"dev-aws": { provider: "aws", region: "us-west-2", credentialSource: "aws-sdk-default" }
272+
},
273+
backends: {
274+
"mock-backend": {
275+
provider: "aws",
276+
capability: "agent-runtime",
277+
adapter: "mock-agent-runtime",
278+
account: "dev-aws"
279+
}
280+
},
281+
defaults: {
282+
provider: "aws",
283+
accountProfile: "dev-aws",
284+
capability: "agent-runtime",
285+
backend: "mock-backend",
286+
targetMode: "session",
287+
protocol: "a2a",
288+
framework: "smoke",
289+
model: { provider: "test", modelId: "smoke-model" },
290+
runtimeTools: { enabled: ["repo-search"] }
291+
}
292+
}, {
293+
adapters: [mockAdapter([{ taskId: "ignored", type: "task.log", message: "sqlite smoke log" }])]
294+
});
295+
const { client, server } = await createClient(runtime);
296+
try {
297+
const handle = await callJson(client, "spawn_cloud_agent", {
298+
instruction: "run SQLite-backed smoke path",
299+
context: { repo: "agent-dispatch" }
300+
});
301+
expect(handle).toMatchObject({
302+
provider: "aws",
303+
accountProfile: "dev-aws",
304+
capability: "agent-runtime",
305+
backend: "mock-agent-runtime",
306+
cloudAgent: {
307+
protocol: "a2a",
308+
sessionId: "agentcore_session_mock"
309+
}
310+
});
311+
312+
const task = await waitForTerminalStatus(client, handle.taskId);
313+
expect(task).toMatchObject({
314+
status: "succeeded",
315+
taskType: "agent.run",
316+
target: { mode: "session", protocol: "a2a" },
317+
input: {
318+
instruction: "run SQLite-backed smoke path",
319+
context: { repo: "agent-dispatch" },
320+
framework: "smoke",
321+
model: { provider: "test", modelId: "smoke-model" },
322+
runtime_tools: { enabled: ["repo-search"] }
323+
}
324+
});
325+
await expect(callJson(client, "get_task_logs", { task_id: handle.taskId })).resolves.toMatchObject({
326+
data: "sqlite smoke log\n"
327+
});
328+
await expect(callJson(client, "get_task_result", { task_id: handle.taskId })).resolves.toMatchObject({
329+
status: "succeeded",
330+
result: { ok: true }
331+
});
332+
} finally {
333+
await client.close();
334+
await server.close();
335+
}
336+
});
255337
});

0 commit comments

Comments
 (0)