diff --git a/.changeset/reliable-mcp-operation-results.md b/.changeset/reliable-mcp-operation-results.md new file mode 100644 index 00000000000..8b84cf51549 --- /dev/null +++ b/.changeset/reliable-mcp-operation-results.md @@ -0,0 +1,5 @@ +--- +"@agent-native/core": patch +--- + +Preserve complete structured MCP action results for clients without inline apps, so mutation receipts remain available when display text is shortened. diff --git a/.changeset/static-registry-audit-tools.md b/.changeset/static-registry-audit-tools.md new file mode 100644 index 00000000000..bae1938562f --- /dev/null +++ b/.changeset/static-registry-audit-tools.md @@ -0,0 +1,5 @@ +--- +"@agent-native/core": patch +--- + +Include core actions in the agent tool catalog when an app supplies a static action registry, so audit history remains discoverable while respecting disabled framework tool groups. diff --git a/packages/core/src/action.ts b/packages/core/src/action.ts index 151ac3d07b3..c1549e55cfb 100644 --- a/packages/core/src/action.ts +++ b/packages/core/src/action.ts @@ -437,6 +437,8 @@ export interface ActionMcpAppResourceConfig { } export interface ActionMcpAppConfig { + /** Preserve the sanitized object result alongside concise text, even without an inline app. Use for durable mutation receipts. */ + structuredContent?: boolean; /** * Optional MCP Apps UI resource for hosts that render inline app iframes. * Required when the action should open an interactive app view. Omit when @@ -1125,7 +1127,11 @@ export function defineAction(options: any) { return undefined; } // compactCatalog-only: no resource required; just keep the flag. - if (options.mcpApp.compactCatalog === true && !options.mcpApp.resource) { + if ( + (options.mcpApp.compactCatalog === true || + options.mcpApp.structuredContent === true) && + !options.mcpApp.resource + ) { return options.mcpApp as ActionMcpAppConfig; } // Full resource: validate html is present. diff --git a/packages/core/src/agent/production-agent.spec.ts b/packages/core/src/agent/production-agent.spec.ts index 9abe5342d0b..7d6c04b5130 100644 --- a/packages/core/src/agent/production-agent.spec.ts +++ b/packages/core/src/agent/production-agent.spec.ts @@ -1278,6 +1278,39 @@ describe("buildUserContentWithAttachments", () => { expect(writeTool.description).toContain("Plan mode blocked"); }); + it("keeps object-only union actions available to the in-app agent", () => { + const anyOf = [ + { + type: "object", + properties: { operation: { const: "create" } }, + required: ["operation"], + }, + { + type: "object", + properties: { operation: { const: "update" } }, + required: ["operation"], + }, + ]; + const tools = actionsToEngineTools({ + setup: { + tool: { + description: "Configure a database", + parameters: { anyOf } as any, + }, + run: async () => ({}), + }, + scalar: { + tool: { + description: "Invalid tool", + parameters: { type: "string" } as any, + }, + run: async () => ({}), + }, + }); + expect(tools.map((tool) => tool.name)).toEqual(["setup"]); + expect(tools[0].inputSchema).toMatchObject({ type: "object", anyOf }); + }); + it("keeps the default initial catalog to discovery/runtime tools", () => { const tools = actionsToEngineTools( attachToolSearch({ diff --git a/packages/core/src/agent/production-agent.ts b/packages/core/src/agent/production-agent.ts index ce15505a0d1..2e4d4c16095 100644 --- a/packages/core/src/agent/production-agent.ts +++ b/packages/core/src/agent/production-agent.ts @@ -40,6 +40,7 @@ import { preUploadAttachments } from "../file-upload/pre-upload-attachments.js"; import { isMcpActionResult } from "../mcp-client/app-result.js"; import { extractMcpToolResultImages } from "../mcp-client/index.js"; import { isMcpToolAllowedForRequest } from "../mcp-client/visibility.js"; +import { isObjectOnly } from "../mcp/tool-input-schema.js"; import { shouldInferSentimentForTurn } from "../observability/sentiment.js"; import { completeRun as completeProgressRun, @@ -3744,7 +3745,7 @@ function normalizeToolInputSchema( schema: ActionTool["parameters"] | undefined, ): EngineTool["inputSchema"] | null { if (!schema) return { type: "object", properties: {} }; - if (schema.type !== "object") return null; + if (!isObjectOnly(schema)) return null; type ToolParams = NonNullable; let cloned: ToolParams; try { diff --git a/packages/core/src/mcp/build-server.ts b/packages/core/src/mcp/build-server.ts index 454bf45ca8a..34f0e46ef7a 100644 --- a/packages/core/src/mcp/build-server.ts +++ b/packages/core/src/mcp/build-server.ts @@ -2388,8 +2388,9 @@ export async function createMCPServerForRequest( Array.isArray(toolVisibility) && toolVisibility.length > 0 && toolVisibility.every((v) => v === "app"); - const readOnlyStructuredResult = - entry.readOnly === true && + const structuredResult = + (entry.readOnly === true || + entry.mcpApp?.structuredContent === true) && rawResultForClient && typeof rawResultForClient === "object" ? Array.isArray(rawResultForClient) @@ -2403,11 +2404,8 @@ export async function createMCPServerForRequest( typeof rawResult === "object" && !Array.isArray(rawResult) ? (rawResult as Record) - : readOnlyStructuredResult - ? mcpAppStructuredContent( - readOnlyStructuredResult, - responseMeta, - ) + : structuredResult + ? mcpAppStructuredContent(structuredResult, responseMeta) : undefined; const text = mcpAppResource ? conciseMcpAppToolText(name, resultForClient, structuredContent!) diff --git a/packages/core/src/mcp/server.spec.ts b/packages/core/src/mcp/server.spec.ts index fe8db8e86c3..45e02e28dbb 100644 --- a/packages/core/src/mcp/server.spec.ts +++ b/packages/core/src/mcp/server.spec.ts @@ -3871,22 +3871,20 @@ describe("handleMcpRequest — web-standard runtime fallback (no Node req/res)", expect(out.result.content[0].text).not.toContain("embed-session-ticket"); }); - it("does NOT surface raw result via structuredContent for model-visible (non-app-only) tools", async () => { - // Counter-regression: only `visibility: ["app"]` tools get the raw - // structuredContent escape hatch. Tools the LLM can call must continue - // to go through the normal text + purge path so embed-start URLs and - // other internal fields stay hidden from the model. + it("preserves complete mutation receipts while sanitizing model-visible structured results", async () => { const embedConfig = { ...config, actions: { "model-callable-helper": { + mcpApp: { structuredContent: true }, tool: { description: "A normal model-visible tool", // No `visibility` hint = model + app visible. }, run: async () => ({ startUrl: "/_agent-native/embed/start?ticket=should-be-hidden", - payload: "ok", + payload: "x".repeat(3000), + receipt: { id: "operation-42", verified: true }, }), }, }, @@ -3906,7 +3904,13 @@ describe("handleMcpRequest — web-standard runtime fallback (no Node req/res)", ); expect(out.error).toBeUndefined(); - expect(out.result.structuredContent).toBeUndefined(); + expect(out.result.structuredContent).toEqual({ + payload: "x".repeat(3000), + receipt: { id: "operation-42", verified: true }, + }); + expect(JSON.stringify(out.result.structuredContent)).not.toContain( + "should-be-hidden", + ); expect(out.result.content[0].text).not.toContain("should-be-hidden"); }); diff --git a/packages/core/src/mcp/tool-input-schema.ts b/packages/core/src/mcp/tool-input-schema.ts index ec915118327..4859aa1e0b8 100644 --- a/packages/core/src/mcp/tool-input-schema.ts +++ b/packages/core/src/mcp/tool-input-schema.ts @@ -1,6 +1,6 @@ import type { Tool } from "@modelcontextprotocol/server"; -function isObjectOnly( +export function isObjectOnly( schema: unknown, ancestors = new Set(), ): boolean { diff --git a/packages/core/src/server/action-discovery.spec.ts b/packages/core/src/server/action-discovery.spec.ts index f89e16ed773..8a7d68d714b 100644 --- a/packages/core/src/server/action-discovery.spec.ts +++ b/packages/core/src/server/action-discovery.spec.ts @@ -4,7 +4,10 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { resolveFrameworkTools } from "../framework-tools.js"; +import { + filterFrameworkToolGroups, + resolveFrameworkTools, +} from "../framework-tools.js"; import { ALWAYS_ON_CORE_ACTIONS, autoDiscoverActions, @@ -68,6 +71,29 @@ describe("action discovery", () => { expect(registry["mutating-read"].readOnly).toBe(false); }); + it( + "makes audit reads available with a static registry while respecting disabled groups", + async () => { + const registry = loadActionsFromStaticRegistry({}); + await mergeCoreSharingActions(registry); + const enabled = filterFrameworkToolGroups( + registry, + resolveFrameworkTools({}).disabledGroups, + ); + const disabled = filterFrameworkToolGroups( + registry, + resolveFrameworkTools({ frameworkTools: { audit: false } }) + .disabledGroups, + ); + for (const name of ["list-audit-events", "get-audit-event"]) { + expect(enabled[name]?.readOnly).toBe(true); + expect(disabled[name]).toBeUndefined(); + expect(registry[name]).toBeDefined(); + } + }, + CORE_ACTION_DISCOVERY_TIMEOUT_MS, + ); + it("preserves grounding metadata from static action entries", () => { const registry = loadActionsFromStaticRegistry({ "grounded-query": { diff --git a/packages/core/src/server/agent-chat-plugin.surface.spec.ts b/packages/core/src/server/agent-chat-plugin.surface.spec.ts index a42acf268bf..c0f6f05ae1f 100644 --- a/packages/core/src/server/agent-chat-plugin.surface.spec.ts +++ b/packages/core/src/server/agent-chat-plugin.surface.spec.ts @@ -542,6 +542,16 @@ describe("framework tool gating — wiring guards", () => { encoding: "utf-8", }); + it("merges core actions before filtering an explicit agent registry", () => { + const merge = source.indexOf( + "await mergeCoreSharingActions(templateScriptsAll);", + ); + expect(merge).toBeGreaterThan(source.indexOf("const rawActions =")); + expect(merge).toBeLessThan( + source.indexOf("filterAgentTools(templateScriptsAll)"), + ); + }); + it("resolves the framework tool surface once and gates both agent registries", () => { expect(source).toContain( "const frameworkTools = resolveFrameworkTools(options);", diff --git a/packages/core/src/server/agent-chat-plugin.ts b/packages/core/src/server/agent-chat-plugin.ts index 237a4032bf6..0702e3d5ff9 100644 --- a/packages/core/src/server/agent-chat-plugin.ts +++ b/packages/core/src/server/agent-chat-plugin.ts @@ -934,6 +934,8 @@ export function createAgentChatPlugin( } catch { // Package action registration is optional. } + const { mergeCoreSharingActions } = await import("./action-discovery.js"); + await mergeCoreSharingActions(templateScriptsAll); // Resource, chat, docs, db, and cross-agent scripts are available in both // prod and dev modes, unless the app switched the group off through diff --git a/scripts/guard-db-tool-scoping.mjs b/scripts/guard-db-tool-scoping.mjs index 17e770f3354..d388dd0a132 100644 --- a/scripts/guard-db-tool-scoping.mjs +++ b/scripts/guard-db-tool-scoping.mjs @@ -47,6 +47,8 @@ const SKIP_DIRS = new Set([ // access is mediated through a scoped parent, custom action, public token, or // cache pathway. Key format: "