@agentproto/adapter-mastra-agent 0.5.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,8 @@ import { ProviderHistoryCompat } from '@mastra/core/processors';
17
17
  import { AgentController } from '@mastra/core/agent-controller';
18
18
  import { Workspace } from '@mastra/core/workspace';
19
19
  import { createNotificationInboxTool } from '@mastra/core/notifications';
20
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
21
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
20
22
  import { SignalProvider } from '@mastra/core/signals';
21
23
  import { PROTOCOL_VERSION, ndJsonStream, AgentSideConnection } from '@agentclientprotocol/sdk';
22
24
  import { Writable, Readable } from 'stream';
@@ -334,6 +336,19 @@ function makeWorkspaceTools(opts) {
334
336
  const cwd = resolve(opts.cwd);
335
337
  const allowExec = opts.allowExec ?? true;
336
338
  const execTimeoutMs = opts.execTimeoutMs ?? 12e4;
339
+ const additionalReadPaths = new Set(
340
+ (opts.additionalReadPaths ?? []).filter(isAbsolute).map((p) => resolve(p))
341
+ );
342
+ const resolveForWrite = (p) => resolveInCwd(cwd, p);
343
+ const resolveForRead = (p) => {
344
+ try {
345
+ return resolveInCwd(cwd, p);
346
+ } catch (err) {
347
+ const target = isAbsolute(p) ? resolve(p) : resolve(cwd, p);
348
+ if (additionalReadPaths.has(target)) return target;
349
+ throw err;
350
+ }
351
+ };
337
352
  const execEnv = { ...process.env, GIT_CEILING_DIRECTORIES: resolve(cwd, "..") };
338
353
  const guard = (id, execute) => withTimeoutGuard(id, execTimeoutMs, execute);
339
354
  const doListDir = async (input) => {
@@ -344,11 +359,11 @@ function makeWorkspaceTools(opts) {
344
359
  };
345
360
  };
346
361
  const doReadFile = async (input) => {
347
- const file = resolveInCwd(cwd, input.path);
362
+ const file = resolveForRead(input.path);
348
363
  return { content: await promises.readFile(file, "utf8") };
349
364
  };
350
365
  const doWriteFile = async (input) => {
351
- const file = resolveInCwd(cwd, input.path);
366
+ const file = resolveForWrite(input.path);
352
367
  await promises.mkdir(resolve(file, ".."), { recursive: true });
353
368
  await promises.writeFile(file, input.content, "utf8");
354
369
  return { path: input.path, bytes: Buffer.byteLength(input.content, "utf8") };
@@ -419,7 +434,7 @@ function makeWorkspaceTools(opts) {
419
434
  }),
420
435
  outputSchema: z.object({ path: z.string(), replaced: z.boolean() }),
421
436
  execute: guard("edit_file", async (input) => {
422
- const file = resolveInCwd(cwd, input.path);
437
+ const file = resolveForWrite(input.path);
423
438
  const current = await promises.readFile(file, "utf8");
424
439
  const count = current.split(input.old_string).length - 1;
425
440
  if (count === 0) throw new Error(`old_string not found in '${input.path}'.`);
@@ -445,7 +460,7 @@ function makeWorkspaceTools(opts) {
445
460
  created: z.string()
446
461
  }),
447
462
  execute: guard("file_info", async (input) => {
448
- const abs = resolveInCwd(cwd, input.path);
463
+ const abs = resolveForRead(input.path);
449
464
  const info = await promises.stat(abs);
450
465
  return {
451
466
  name: abs.split(sep).pop() ?? input.path,
@@ -552,7 +567,7 @@ function makeWorkspaceTools(opts) {
552
567
  outputSchema: z.object({ diff: z.string() }),
553
568
  execute: guard("read_diff", async (input) => {
554
569
  const relPaths = (input.paths ?? []).map((p) => {
555
- const abs = resolveInCwd(cwd, p);
570
+ const abs = resolveForRead(p);
556
571
  return relative(cwd, abs) || ".";
557
572
  });
558
573
  const args = [
@@ -583,7 +598,7 @@ function makeWorkspaceTools(opts) {
583
598
  outputSchema: z.object({ applied: z.boolean(), output: z.string() }),
584
599
  execute: guard("apply_patch", async (input) => {
585
600
  for (const p of extractPatchPaths(input.patch)) {
586
- resolveInCwd(cwd, p);
601
+ resolveForWrite(p);
587
602
  }
588
603
  const patchFile = join(tmpdir(), `mastra-agent-patch-${randomUUID()}.diff`);
589
604
  await promises.writeFile(patchFile, input.patch, "utf8");
@@ -787,6 +802,61 @@ function resolveMastraModel(ref, env = process.env) {
787
802
  }
788
803
  return modelId;
789
804
  }
805
+ async function connectDaemonMcpClient(opts = {}) {
806
+ const endpoint = await discoverDaemonEndpoint(opts);
807
+ if (!endpoint) throw new DaemonNotFoundError();
808
+ const env = opts.env ?? process.env;
809
+ const url = new URL(`${endpoint.url}/mcp`);
810
+ if (env.AGENTPROTO_SESSION_ID) url.searchParams.set("callerSessionId", env.AGENTPROTO_SESSION_ID);
811
+ const client = new Client({ name: "agentproto-mastra-agent", version: "0.1.0" }, { capabilities: {} });
812
+ const transport = new StreamableHTTPClientTransport(url, {
813
+ ...endpoint.token ? { requestInit: { headers: { authorization: `Bearer ${endpoint.token}` } } } : {}
814
+ });
815
+ await client.connect(transport);
816
+ return client;
817
+ }
818
+ async function listDaemonMcpTools(client) {
819
+ const { tools } = await client.listTools();
820
+ return tools.map((t) => ({
821
+ name: t.name,
822
+ ...t.description !== void 0 ? { description: t.description } : {},
823
+ inputSchema: t.inputSchema ?? { type: "object" }
824
+ }));
825
+ }
826
+ function injectAppId(toolName, args, appId) {
827
+ if (!appId) return args;
828
+ if (!toolName.startsWith("app_")) return args;
829
+ if (args.appId !== void 0) return args;
830
+ return { ...args, appId };
831
+ }
832
+ function relaxAppIdRequirement(inputSchema, toolName, appId) {
833
+ if (!appId || !toolName.startsWith("app_")) return inputSchema;
834
+ if (!Array.isArray(inputSchema.required) || !inputSchema.required.includes("appId")) return inputSchema;
835
+ return { ...inputSchema, required: inputSchema.required.filter((id) => id !== "appId") };
836
+ }
837
+ function extractText(content) {
838
+ if (!Array.isArray(content)) return "";
839
+ return content.filter(
840
+ (block) => !!block && typeof block === "object" && block.type === "text" && typeof block.text === "string"
841
+ ).map((block) => block.text).join("\n");
842
+ }
843
+ function makeDaemonMcpProxyTool(def, opts) {
844
+ const config = {
845
+ id: def.name,
846
+ description: def.description ?? `Daemon tool "${def.name}", proxied via the daemon's MCP gateway.`,
847
+ inputSchema: relaxAppIdRequirement(def.inputSchema, def.name, opts.appId),
848
+ execute: async (input) => {
849
+ const client = await opts.getClient();
850
+ const args = injectAppId(def.name, input ?? {}, opts.appId);
851
+ const result = await client.callTool({ name: def.name, arguments: args });
852
+ if (result.isError) {
853
+ throw new Error(`daemon tool "${def.name}" failed: ${extractText(result.content) || "(no error detail)"}`);
854
+ }
855
+ return result.structuredContent ?? extractText(result.content);
856
+ }
857
+ };
858
+ return createTool(config);
859
+ }
790
860
  var WATCHED_EVENT_KINDS = [
791
861
  "turn-end",
792
862
  "error",
@@ -1352,6 +1422,18 @@ var DEFAULT_TOOL_IDS = [
1352
1422
  function envFlag(value) {
1353
1423
  return Boolean(value);
1354
1424
  }
1425
+ function parseAdditionalReadPathsEnv(env = process.env) {
1426
+ const raw = env.AGENTPROTO_ADDITIONAL_READ_PATHS;
1427
+ if (!raw) return void 0;
1428
+ try {
1429
+ const parsed = JSON.parse(raw);
1430
+ if (!Array.isArray(parsed)) return void 0;
1431
+ const paths = parsed.filter((p) => typeof p === "string" && p.length > 0);
1432
+ return paths.length > 0 ? paths : void 0;
1433
+ } catch {
1434
+ return void 0;
1435
+ }
1436
+ }
1355
1437
  function defaultAgentManifest(model) {
1356
1438
  return [
1357
1439
  "---",
@@ -1429,8 +1511,31 @@ function makeAgentFactory(opts = {}) {
1429
1511
  const workspaceTools = makeWorkspaceTools({
1430
1512
  cwd,
1431
1513
  allowExec: opts.allowExec,
1514
+ // The daemon's exact-file AGENTS.md read grant (pointer-mode contract)
1515
+ // — explicit option wins, else the driver-set env var.
1516
+ additionalReadPaths: opts.additionalReadPaths ?? parseAdditionalReadPathsEnv(),
1432
1517
  extraTools: opts.extraTools
1433
1518
  });
1519
+ const modesEnabled = opts.modes !== false && !envFlag(process.env.AGENTPROTO_MASTRA_NO_MODES);
1520
+ const daemonClient = new DaemonClient({ cwd });
1521
+ const daemonSubAgentTools = modesEnabled ? makeDaemonTools({ client: daemonClient }) : {};
1522
+ let daemonMcpClient;
1523
+ let daemonMcpToolDefsPromise;
1524
+ const resolvedDaemonMcpTools = {};
1525
+ const appId = opts.appId ?? process.env.AGENTPROTO_APP_ID;
1526
+ async function getDaemonMcpToolDefs() {
1527
+ if (!daemonMcpToolDefsPromise) {
1528
+ daemonMcpToolDefsPromise = (async () => {
1529
+ try {
1530
+ daemonMcpClient = opts.daemonMcp?.client ?? await connectDaemonMcpClient({ cwd, ...opts.daemonMcp?.discoverOptions });
1531
+ return await listDaemonMcpTools(daemonMcpClient);
1532
+ } catch {
1533
+ return [];
1534
+ }
1535
+ })();
1536
+ }
1537
+ return daemonMcpToolDefsPromise;
1538
+ }
1434
1539
  const store = buildSqliteStore();
1435
1540
  let memory;
1436
1541
  const historyCompat = new ProviderHistoryCompat({
@@ -1439,21 +1544,38 @@ function makeAgentFactory(opts = {}) {
1439
1544
  const { agent } = await buildMastraAgent(handle, {
1440
1545
  inputProcessors: [historyCompat],
1441
1546
  resolveModel: (ref) => resolveMastraModel(ref),
1442
- // Match each declared tool ref against the workspace toolset by id. A
1443
- // ref with no matching executor still resolves to a stub that fails
1444
- // fast and clearly on call rather than being dropped: a dropped ref
1445
- // leaves the model unable to see it at all, and (if the model still
1446
- // tries the name from AGENT.md prose) surfaces as an opaque provider
1447
- // NoSuchToolError this adapter's ACP layer silently swallows (see
1448
- // tool-call-map.ts), which is how a declared-but-unwired tool used to
1449
- // hang a turn with zero recorded tool calls instead of failing fast.
1450
- resolveTool: (ref) => {
1547
+ // Match each declared tool ref, in order: the workspace toolset, the
1548
+ // curated daemon sub-agent tools, then (modes-on only) the generic
1549
+ // daemon MCP proxy ONLY for a ref an AGENT.md actually declares,
1550
+ // which is what keeps the proxy an allowlist rather than a blanket
1551
+ // grant of the daemon's whole toolset. A ref matching none of those
1552
+ // still resolves to a stub that fails fast and clearly on call —
1553
+ // rather than being dropped: a dropped ref leaves the model unable to
1554
+ // see it at all, and (if the model still tries the name from AGENT.md
1555
+ // prose) surfaces as an opaque provider NoSuchToolError this adapter's
1556
+ // ACP layer silently swallows (see tool-call-map.ts), which is how a
1557
+ // declared-but-unwired tool used to hang a turn with zero recorded
1558
+ // tool calls instead of failing fast.
1559
+ resolveTool: async (ref) => {
1451
1560
  const id = toolRefId(ref);
1452
1561
  if (!id) return void 0;
1453
- const tool = workspaceTools[id];
1454
- if (tool) return { name: id, tool };
1562
+ const workspaceTool = workspaceTools[id];
1563
+ if (workspaceTool) return { name: id, tool: workspaceTool };
1564
+ const daemonSubAgentTool = daemonSubAgentTools[id];
1565
+ if (daemonSubAgentTool) return { name: id, tool: daemonSubAgentTool };
1566
+ if (modesEnabled) {
1567
+ const def = (await getDaemonMcpToolDefs()).find((d) => d.name === id);
1568
+ if (def) {
1569
+ const tool = makeDaemonMcpProxyTool(def, {
1570
+ getClient: async () => daemonMcpClient,
1571
+ ...appId !== void 0 ? { appId } : {}
1572
+ });
1573
+ resolvedDaemonMcpTools[id] = tool;
1574
+ return { name: id, tool };
1575
+ }
1576
+ }
1455
1577
  console.warn(
1456
- `[@agentproto/adapter-mastra-agent] agent '${handle.id}' declares tool '${id}' but no executor is wired for it in this adapter's workspace toolset \u2014 calls to it will fail immediately instead of hanging. Wire it in workspace-tools.ts or pass it via extraTools.`
1578
+ `[@agentproto/adapter-mastra-agent] agent '${handle.id}' declares tool '${id}' but no executor is wired for it (not in the workspace toolset, the daemon sub-agent tools, or the daemon's own MCP tool list) \u2014 calls to it will fail immediately instead of hanging. Wire it in workspace-tools.ts, pass it via extraTools, or expose it from the daemon.`
1457
1579
  );
1458
1580
  return { name: id, tool: makeUnwiredToolStub(id) };
1459
1581
  },
@@ -1461,12 +1583,10 @@ function makeAgentFactory(opts = {}) {
1461
1583
  // The markdown body is the agent's primary system prompt (AIP-42).
1462
1584
  body
1463
1585
  });
1464
- const modesEnabled = opts.modes !== false && !envFlag(process.env.AGENTPROTO_MASTRA_NO_MODES);
1465
1586
  const { modes, defaultModeId } = modesEnabled ? resolveModes(body) : { modes: [{ id: "main", metadata: { default: true } }], defaultModeId: "main" };
1466
1587
  let tools = workspaceTools;
1467
1588
  if (modesEnabled) {
1468
- const daemonClient = new DaemonClient({ cwd });
1469
- tools = { ...workspaceTools, ...makeDaemonTools({ client: daemonClient }) };
1589
+ tools = { ...workspaceTools, ...daemonSubAgentTools, ...resolvedDaemonMcpTools };
1470
1590
  const signalProvider = new AgentprotoSignalProvider({
1471
1591
  client: daemonClient,
1472
1592
  stateEmitter: new DaemonStateEmitter({ client: daemonClient, cwd })
@@ -1628,6 +1748,7 @@ var SUSPENSION_OPTIONS = [
1628
1748
  var META_SUSPEND_PAYLOAD = "mastra-agent/suspendPayload";
1629
1749
  var META_RESUME_SCHEMA = "mastra-agent/resumeSchema";
1630
1750
  var META_RESUME_DATA = "mastra-agent/resumeData";
1751
+ var META_FEEDBACK = "agentproto/feedback";
1631
1752
  function emptySessionState() {
1632
1753
  return {
1633
1754
  session: null,
@@ -1781,6 +1902,19 @@ var MastraAcpAgent = class {
1781
1902
  state.turn = turn;
1782
1903
  const { text, files } = promptContent(params);
1783
1904
  try {
1905
+ if (!interrupted && state.suspensions.size > 0) {
1906
+ await this.#conn.sessionUpdate({
1907
+ sessionId: params.sessionId,
1908
+ update: {
1909
+ sessionUpdate: "agent_message_chunk",
1910
+ content: {
1911
+ type: "text",
1912
+ text: "\n[mastra-agent] run is suspended awaiting approval \u2014 resolve the pending permission request (or cancel the session) before sending another prompt.\n"
1913
+ }
1914
+ }
1915
+ });
1916
+ return { stopReason: "refusal" };
1917
+ }
1784
1918
  const session = await this.#ensureSession(params.sessionId, state);
1785
1919
  const map = createEventMapper();
1786
1920
  let endReason;
@@ -1798,6 +1932,21 @@ var MastraAcpAgent = class {
1798
1932
  if (event.type === "agent_end") {
1799
1933
  endReason = event.reason ?? "complete";
1800
1934
  resolveAgentEnd?.();
1935
+ if (event.reason === "suspended") {
1936
+ relay = relay.then(
1937
+ () => this.#conn.sessionUpdate({
1938
+ sessionId: params.sessionId,
1939
+ update: {
1940
+ sessionUpdate: "agent_message_chunk",
1941
+ content: {
1942
+ type: "text",
1943
+ text: "\n[mastra-agent] run suspended \u2014 waiting for approval before continuing.\n"
1944
+ }
1945
+ }
1946
+ })
1947
+ ).catch(() => {
1948
+ });
1949
+ }
1801
1950
  return;
1802
1951
  }
1803
1952
  if (event.type === "tool_approval_required") {
@@ -1994,7 +2143,11 @@ var MastraAcpAgent = class {
1994
2143
  if (pending.cancelled) return;
1995
2144
  if (outcome.outcome !== "selected") return;
1996
2145
  const meta = outcome._meta;
1997
- const resumeData = meta && META_RESUME_DATA in meta ? meta[META_RESUME_DATA] : { approved: outcome.optionId === "approve" };
2146
+ const feedback = meta && typeof meta[META_FEEDBACK] === "string" ? meta[META_FEEDBACK] : void 0;
2147
+ const resumeData = meta && META_RESUME_DATA in meta ? meta[META_RESUME_DATA] : {
2148
+ approved: outcome.optionId === "approve",
2149
+ ...feedback ? { feedback } : {}
2150
+ };
1998
2151
  await session.respondToToolSuspension({
1999
2152
  resumeData,
2000
2153
  toolCallId: event.toolCallId
@@ -2028,6 +2181,12 @@ var MastraAcpAgent = class {
2028
2181
  threadId: acpSessionId
2029
2182
  });
2030
2183
  session.subscribe((event) => {
2184
+ if (event.type === "tool_suspension_cancelled") {
2185
+ const pending = state.suspensions.get(event.toolCallId);
2186
+ if (pending) pending.cancelled = true;
2187
+ state.suspensions.delete(event.toolCallId);
2188
+ return;
2189
+ }
2031
2190
  if (event.type === "mode_changed") {
2032
2191
  void this.#conn.sessionUpdate({
2033
2192
  sessionId: acpSessionId,
@@ -2091,5 +2250,5 @@ function runAcpOverStdio(buildController) {
2091
2250
  }
2092
2251
 
2093
2252
  export { DEFAULT_MODEL, DEFAULT_MODEL_CATALOG, DEFAULT_TOOL_IDS, DISABLED_BUILTIN_TOOL_IDS, DaemonClient, DaemonHttpError, DaemonNotFoundError, MastraAcpAgent, buildSqliteMemory, buildSqliteStore, createEventMapper, defaultAgentManifest, discoverDaemonEndpoint, makeAgentFactory, makeDaemonTools, makeWorkspaceTools, messageText, modelRefToString, promptContent, promptText, providerOf, resolveInCwd, resolveMastraModel, resolveMemoryDbPath, runAcpOverStdio, toolCallTitle, toolKindFor };
2094
- //# sourceMappingURL=chunk-6ILIPWNM.mjs.map
2095
- //# sourceMappingURL=chunk-6ILIPWNM.mjs.map
2253
+ //# sourceMappingURL=chunk-2FKFITFT.mjs.map
2254
+ //# sourceMappingURL=chunk-2FKFITFT.mjs.map