@agent-native/core 0.209.0-nightly-20261014124140 → 0.209.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.
Files changed (53) hide show
  1. package/dist/agent/production-agent.d.ts +8 -0
  2. package/dist/agent/production-agent.js +68 -3
  3. package/dist/agent/strict-tool-input.d.ts +4 -0
  4. package/dist/agent/strict-tool-input.js +26 -0
  5. package/dist/agent/types.d.ts +7 -0
  6. package/dist/cli/skills-content/turn-into-app-attachments.d.ts +1 -1
  7. package/dist/cli/skills-content/turn-into-app-attachments.js +1 -3
  8. package/dist/cli/skills-content/turn-into-app-skill.d.ts +2 -2
  9. package/dist/cli/skills-content/turn-into-app-skill.js +15 -10
  10. package/dist/client/chat/agentkit-protocol.js +2 -0
  11. package/dist/client/chat/runtime.d.ts +1 -0
  12. package/dist/client/chat/runtime.js +35 -3
  13. package/dist/collab/routes.d.ts +2 -2
  14. package/dist/collab/struct-routes.d.ts +1 -1
  15. package/dist/mcp/actions/call-mcp-tool.js +1 -1
  16. package/dist/mcp-client/app-api.d.ts +3 -0
  17. package/dist/mcp-client/app-api.js +8 -1
  18. package/dist/mcp-client/index.d.ts +7 -2
  19. package/dist/mcp-client/index.js +10 -4
  20. package/dist/mcp-client/manager.d.ts +16 -1
  21. package/dist/mcp-client/manager.js +35 -7
  22. package/dist/mcp-client/routes.js +9 -4
  23. package/dist/observability/metrics.d.ts +2 -0
  24. package/dist/observability/metrics.js +15 -1
  25. package/dist/observability/traces.js +8 -1
  26. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  27. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  28. package/dist/resources/handlers.d.ts +1 -1
  29. package/dist/server/agent-chat/additional-actions.d.ts +45 -0
  30. package/dist/server/agent-chat/additional-actions.js +707 -0
  31. package/dist/server/agent-chat/mcp-glue.d.ts +3 -1
  32. package/dist/server/agent-chat/mcp-glue.js +12 -1
  33. package/dist/server/agent-chat/plugin-options.d.ts +27 -0
  34. package/dist/server/agent-chat-plugin.js +59 -14
  35. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  36. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  37. package/dist/server/better-auth-instance.js +108 -18
  38. package/dist/server/builder-browser.d.ts +4 -2
  39. package/dist/server/builder-browser.js +21 -16
  40. package/dist/server/index.d.ts +2 -1
  41. package/dist/server/index.js +1 -1
  42. package/dist/server/realtime-token.d.ts +1 -1
  43. package/dist/shared/data-url.d.ts +2 -0
  44. package/dist/shared/data-url.js +12 -0
  45. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/attachments.md +1 -3
  46. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +13 -8
  47. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/source-brief.md +2 -2
  48. package/docs/content/agent-surfaces.mdx +61 -0
  49. package/docs/content/observability.mdx +2 -0
  50. package/package.json +5 -4
  51. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/attachments.md +1 -3
  52. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +13 -8
  53. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/source-brief.md +2 -2
@@ -328,14 +328,19 @@ On a browser host the host is an orchestrator, not the build environment:
328
328
  (archetype, direction, first viewport, sample-data plan, agent moments).
329
329
  2. Call \`start-workspace-app-creation\` on the Agent-Native Dispatch MCP
330
330
  connector (\`agent-native-dispatch\`). Put the brief and the design decisions
331
- in \`prompt\`; pass an \`appId\`, a short \`description\`, the \`template\`, the
332
- selected \`resourceIds\`, and source files as \`attachments\`
333
- ([attachments.md](attachments.md)). The tool's schema describes each
334
- argument. Reference resources by id; never paste whole knowledge files or
335
- binary data into \`prompt\`, and do not assume an attachment becomes a file in
336
- the generated workspace. A workbook cannot be attached (attachments take
337
- images, PDF, text, and JSON): pass a bounded CSV rendering as text, or a
338
- public URL, and say what was left out.
331
+ in \`prompt\`; pass an \`appId\`, a short \`description\`, and the \`template\`.
332
+ The tool's schema describes each argument. Send the Project's own text as
333
+ \`attachments\` ([attachments.md](attachments.md)), never as a summary: the
334
+ Project instructions as \`project-instructions.md\` and every knowledge file,
335
+ each verbatim as \`text/plain\`. Never paste whole knowledge files or binary
336
+ data into \`prompt\`. Attachments are model context
337
+ and are not copied into the generated workspace, so tell the Builder agent in
338
+ \`prompt\` to persist what the app needs: knowledge files as app resources, the
339
+ instructions as the in-app agent's instructions. If a file cannot be
340
+ reproduced verbatim (not in context, too large), say so in the brief and the
341
+ report; never swap in a digest silently. A workbook cannot be attached
342
+ (attachments take images, PDF, text, and JSON): pass a bounded CSV rendering
343
+ as text, or a public URL, and say what was left out.
339
344
  3. Report what Dispatch actually returned: the branch, the path, and the status.
340
345
  The browser host cannot run or inspect the app, and a returned path can 404
341
346
  until the branch merges and deploys, so the report ends at pending or
@@ -1180,7 +1185,7 @@ heading; write \`n/a\` where a heading does not apply.
1180
1185
  Source and provenance: type (thread, skill, Project, spreadsheet, export), file names, ids, what was read and what was not
1181
1186
  Project goal: the user and the repeatable job, in one sentence
1182
1187
  Configuration and constraints: audience, rules, output standards, approved tools
1183
- Knowledge sources: reference files the app ships with, by name or id
1188
+ Knowledge sources: reference files the app ships with, by name
1184
1189
  Repeatable workflow: the job's trigger, steps, and outcome, in the source's own words
1185
1190
  Inputs and outputs: what comes in, what the user gets back, fixed output shapes
1186
1191
  Judgment and review points: 1-3 agent moments, each tied to the object it acts on; what the user confirms
@@ -1198,7 +1203,7 @@ App design: archetype, direction, first-viewport sketch, sample-data plan
1198
1203
 
1199
1204
  - Name the latest successful, repeatable job. A walkthrough of one example is
1200
1205
  evidence for the job, not the job.
1201
- - Keep references bounded: a file name, URL, resource id, or short summary
1206
+ - Keep references bounded: a file name, URL, or short summary
1202
1207
  instead of a pasted document. No secrets, credentials, or customer data.
1203
1208
  - Corrections the user made in the source are product rules. Record each one
1204
1209
  as an invariant.
@@ -587,6 +587,7 @@ function runtimeMessageToProtocolMessage(message, fallbackTextFormat) {
587
587
  role: message.role,
588
588
  parts: message.content.map((part) => runtimePartToProtocolPart(part, textFormat)),
589
589
  createdAt: message.createdAt,
590
+ status: message.status,
590
591
  metadata: message.metadata,
591
592
  };
592
593
  }
@@ -677,6 +678,7 @@ function protocolMessageToRuntimeMessage(message) {
677
678
  role: message.role,
678
679
  content: message.parts.map(protocolPartToRuntimePart),
679
680
  createdAt: message.createdAt,
681
+ status: message.status,
680
682
  metadata: message.metadata,
681
683
  };
682
684
  }
@@ -78,6 +78,7 @@ export interface AgentChatRuntimeMessage<TContentPart extends AgentChatRuntimeCo
78
78
  readonly role: AgentChatRuntimeRole;
79
79
  readonly content: readonly TContentPart[];
80
80
  readonly createdAt?: string;
81
+ readonly status?: "streaming" | "complete" | "error";
81
82
  readonly metadata?: AgentChatRuntimeMetadata;
82
83
  }
83
84
  export interface AgentChatRuntimeAttachment {
@@ -1,5 +1,6 @@
1
1
  import { AgentKitRunSlotBusyError } from "@agent-native/agentkit/client";
2
2
  import { AUTO_CONTINUE_OF_RUN_METADATA_KEY, CONTINUE_OF_RUN_METADATA_KEY, } from "../../agent/auto-continue.js";
3
+ import { RUN_NOT_STARTED_METADATA_KEY } from "../../shared/agent-chat-run-not-started.js";
3
4
  import { agentEngineStatusUrlForChatApi, requireAgentEngineConfiguredForDispatch, } from "../agent-engine-readiness.js";
4
5
  import { getOrCreateAnalyticsSessionId } from "../analytics-session.js";
5
6
  import { agentChatStreamingUrl, agentNativePath } from "../api-path.js";
@@ -770,6 +771,36 @@ function isSyntheticToolHistoryOmissionMessage(message) {
770
771
  (message.id === AGENTKIT_TOOL_HISTORY_OMISSION_MESSAGE_ID_PREFIX ||
771
772
  /^agentkit-tool-history-omission-\d+$/.test(message.id)));
772
773
  }
774
+ function isUnstartedNativeHistoryMessage(message) {
775
+ if (message.role === "user" && message.status === "error")
776
+ return true;
777
+ if (message.role !== "user" && message.role !== "assistant")
778
+ return false;
779
+ const custom = asRecord(asRecord(message.metadata)?.custom);
780
+ return custom?.[RUN_NOT_STARTED_METADATA_KEY] === true;
781
+ }
782
+ function nativeHistorySourceMessages(messages, currentPrompt) {
783
+ const source = messages ?? [];
784
+ let currentPromptMessageIndex;
785
+ for (let index = source.length - 1; index >= 0; index--) {
786
+ const message = source[index];
787
+ if (isSyntheticToolHistoryOmissionMessage(message))
788
+ continue;
789
+ if (message.role !== "user")
790
+ continue;
791
+ if (!currentPrompt.trim()) {
792
+ currentPromptMessageIndex = index;
793
+ }
794
+ else {
795
+ const match = runtimeMessageTextMatches(message, currentPrompt);
796
+ if (match.matches || !match.complete)
797
+ currentPromptMessageIndex = index;
798
+ }
799
+ break;
800
+ }
801
+ return source.filter((message, index) => index === currentPromptMessageIndex ||
802
+ !isUnstartedNativeHistoryMessage(message));
803
+ }
773
804
  function equivalentRuntimeContent(left, right) {
774
805
  let remaining = 4096;
775
806
  const leftAncestors = new WeakSet();
@@ -818,7 +849,7 @@ function equivalentRuntimeContent(left, right) {
818
849
  return compare(left, right, 0);
819
850
  }
820
851
  function priorNativeHistoryMessages(messages, currentPrompt) {
821
- const source = messages ?? [];
852
+ const source = nativeHistorySourceMessages(messages, currentPrompt);
822
853
  let currentPromptMessageIndex;
823
854
  for (let index = source.length - 1; index >= 0; index--) {
824
855
  const message = source[index];
@@ -1810,6 +1841,7 @@ function assertUnambiguousKeyOnlyApprovedHistory(sources, approvedToolCalls, app
1810
1841
  }
1811
1842
  function nativeStructuredHistoryFromMessages(messages, currentPrompt, currentAttachmentUrls, supplementalMessages = [], supplementalHistoryOmitted = false, supplementalToolHistoryOmitted = false, approvedToolCalls = [], approvedToolCallAskIds = []) {
1812
1843
  const structuredHistory = [];
1844
+ const historyMessages = nativeHistorySourceMessages(messages, currentPrompt);
1813
1845
  const approvedToolCallSet = new Set(approvedToolCalls);
1814
1846
  const approvedToolCallAskIdSet = new Set(approvedToolCallAskIds);
1815
1847
  const callCandidates = [];
@@ -1820,9 +1852,9 @@ function nativeStructuredHistoryFromMessages(messages, currentPrompt, currentAtt
1820
1852
  const textCandidates = [];
1821
1853
  let toolHistoryPosition = 0;
1822
1854
  let hasToolHistory = false;
1823
- const sources = boundedStructuredHistorySources(messages, currentPrompt, supplementalMessages, currentAttachmentUrls);
1855
+ const sources = boundedStructuredHistorySources(historyMessages, currentPrompt, supplementalMessages, currentAttachmentUrls);
1824
1856
  assertUnambiguousKeyOnlyApprovedHistory(sources.messages, approvedToolCalls, approvedToolCallAskIds);
1825
- const boundaryResultsToOmit = boundaryToolResultPartsToOmit(messages, supplementalMessages, sources, supplementalToolHistoryOmitted);
1857
+ const boundaryResultsToOmit = boundaryToolResultPartsToOmit(historyMessages, supplementalMessages, sources, supplementalToolHistoryOmitted);
1826
1858
  for (const { message, parts, priority, textPriority } of sources.messages) {
1827
1859
  if (message.role !== "user" && message.role !== "assistant")
1828
1860
  continue;
@@ -13,16 +13,16 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
13
13
  };
14
14
  }>>;
15
15
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
21
21
  }>>;
22
22
  export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
23
- text?: undefined;
24
23
  ok?: undefined;
25
24
  error: string;
25
+ text?: undefined;
26
26
  } | {
27
27
  error?: undefined;
28
28
  ok: boolean;
@@ -1,6 +1,6 @@
1
1
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
- error: string;
3
2
  ok?: undefined;
3
+ error: string;
4
4
  } | {
5
5
  error?: undefined;
6
6
  ok: boolean;
@@ -11,5 +11,5 @@ export default defineAction({
11
11
  toolName: z.string().trim().min(1),
12
12
  arguments: z.record(z.string(), z.unknown()).default({}),
13
13
  }),
14
- run: ({ serverId, toolName, arguments: args }) => callMcpTool(serverId, toolName, args),
14
+ run: ({ serverId, toolName, arguments: args }, context) => callMcpTool(serverId, toolName, args, { context }),
15
15
  });
@@ -1,3 +1,4 @@
1
+ import type { ActionRunContext } from "../action.js";
1
2
  export interface AppMcpTool {
2
3
  serverId: string;
3
4
  name: string;
@@ -20,6 +21,8 @@ export interface ListVisibleMcpToolsOptions {
20
21
  export interface CallMcpToolOptions {
21
22
  /** Refuse the call unless the server's URL belongs to this provider. */
22
23
  providerId?: string;
24
+ /** Caller identity and invocation source forwarded to MCP authorization. */
25
+ context?: ActionRunContext;
23
26
  }
24
27
  export declare class McpAppApiError extends Error {
25
28
  readonly statusCode: 401 | 403 | 503;
@@ -36,7 +36,14 @@ export async function callMcpTool(serverId, originalToolName, args = {}, options
36
36
  !isServerForProvider(manager, serverId, options.providerId)) {
37
37
  throw new McpAppApiError("MCP tool is not available in this request scope.", 403);
38
38
  }
39
- return manager.callTool(buildMcpToolName(serverId, originalToolName), args);
39
+ return manager.callTool(buildMcpToolName(serverId, originalToolName), args, {
40
+ context: {
41
+ ...options.context,
42
+ caller: options.context?.caller ?? "frontend",
43
+ userEmail: context.userEmail,
44
+ orgId: context.orgId ?? null,
45
+ },
46
+ });
40
47
  }
41
48
  function requireAuthenticatedRequest() {
42
49
  const context = getRequestContext();
@@ -1,5 +1,5 @@
1
1
  export { loadMcpConfig, autoDetectMcpConfig, type McpConfig, type McpServerConfig, } from "./config.js";
2
- export { McpClientManager, buildMcpToolName, parseMcpToolName, MCP_TOOL_PREFIX, type McpTool, type McpClientManagerOptions, } from "./manager.js";
2
+ export { McpClientManager, buildMcpToolName, parseMcpToolName, MCP_TOOL_PREFIX, type McpTool, type McpToolCallOptions, type McpToolAuthorizationResolver, type McpClientManagerOptions, } from "./manager.js";
3
3
  export { listRemoteServers, addRemoteServer, addOAuthRemoteServer, addFirstPartyRemoteServer, isFirstPartyRemoteEndpointTrusted, removeRemoteServer, validateRemoteUrl, normalizeServerName, mergedConfigKey, parseMergedKey, hashEmail, toHttpServerConfig, toHttpServerConfigAsync, materializeHeaders, type RemoteMcpScope, type StoredRemoteMcpServer, } from "./remote-store.js";
4
4
  export { findConnectedMcpServersForProvider, type ConnectedMcpProviderResult, type ConnectedMcpProviderServer, } from "./provider-connections.js";
5
5
  export { finishMcpOAuthAuthorization, getMcpOAuthAccessToken, readMcpOAuthCredentials, saveMcpOAuthCredentials, startMcpOAuthAuthorization, type McpOAuthCallbackResult, type McpOAuthCredentialBundle, type McpOAuthDiscoveryState, type McpOAuthProviderOptions, type McpOAuthStartResult, } from "./oauth-client.js";
@@ -14,14 +14,19 @@ export { callMcpTool, listVisibleMcpTools, McpAppApiError, type AppMcpTool, type
14
14
  export { classifyMcpToolCall, evaluateMcpToolCallPolicy, type McpToolCallClassification, type McpToolEffect, type McpToolFamily, type McpToolInvocationPolicy, type McpToolPolicyDecision, } from "./tool-policy.js";
15
15
  export { configureScreenMemory, queryScreenMemoryForAgent, queryScreenMemoryContext, readScreenMemoryStatus, type ScreenMemoryConfig, type ScreenMemoryAgentQueryResult, type ScreenMemoryContextItem, type ScreenMemoryCoverageGap, type ScreenMemoryEvidenceItem, type ScreenMemoryEvidenceSourceType, type ScreenMemoryQueryResult, type ScreenMemoryRetrievalCoverage, type ScreenMemorySegmentReference, type ScreenMemoryStatus, type ScreenMemoryTimeRange, type ScreenMemoryTruncation, } from "./screen-memory-local.js";
16
16
  export { MCP_ACTION_RESULT_MARKER, isMcpActionResult, type AgentMcpAppPayload, type AgentMcpAppResourceContent, type McpActionResult, } from "./app-result.js";
17
+ import { type ActionAuthorize } from "../action.js";
17
18
  import type { EngineToolResultImagePart } from "../agent/engine/types.js";
18
19
  import type { ActionEntry } from "../agent/production-agent.js";
19
20
  import type { McpClientManager, McpTool } from "./manager.js";
20
21
  import { type McpToolInvocationPolicy } from "./tool-policy.js";
22
+ export interface McpActionEntryResolution extends Partial<Pick<ActionEntry, "needsApproval" | "allowPersistentApproval">> {
23
+ /** Authorization checked on every invocation immediately before the remote call. */
24
+ authorize?: ActionAuthorize<Record<string, unknown>>;
25
+ }
21
26
  export interface McpActionEntryOptions {
22
27
  invocationPolicy?: McpToolInvocationPolicy;
23
28
  toolNames?: readonly string[];
24
- resolveActionEntry?: (tool: McpTool) => Partial<Pick<ActionEntry, "needsApproval" | "allowPersistentApproval">> | undefined;
29
+ resolveActionEntry?: (tool: McpTool) => McpActionEntryResolution | undefined;
25
30
  }
26
31
  export declare function mcpToolsToActionEntries(manager: McpClientManager, options?: McpActionEntryOptions): Record<string, ActionEntry>;
27
32
  export declare function syncMcpActionEntries(manager: McpClientManager, target: Record<string, ActionEntry>, options?: McpActionEntryOptions): void;
@@ -16,7 +16,7 @@ export { classifyMcpToolCall, evaluateMcpToolCallPolicy, } from "./tool-policy.j
16
16
  export { configureScreenMemory, queryScreenMemoryForAgent, queryScreenMemoryContext, readScreenMemoryStatus, } from "./screen-memory-local.js";
17
17
  export { MCP_ACTION_RESULT_MARKER, isMcpActionResult, } from "./app-result.js";
18
18
  import { getToolUiResourceUri, isToolVisibilityAppOnly, isToolVisibilityModelOnly, } from "@modelcontextprotocol/ext-apps/app-bridge";
19
- import { MCP_APP_MIME_TYPE } from "../action.js";
19
+ import { MCP_APP_MIME_TYPE, } from "../action.js";
20
20
  import { normalizeToolResultImages } from "../agent/tool-result-images.js";
21
21
  import { MCP_ACTION_RESULT_MARKER, toolForMcpAppPayload, } from "./app-result.js";
22
22
  import { evaluateMcpToolCallPolicy, } from "./tool-policy.js";
@@ -45,7 +45,7 @@ export function syncMcpActionEntries(manager, target, options = {}) {
45
45
  }
46
46
  }
47
47
  function mcpToolToActionEntry(manager, tool, options = {}) {
48
- const resolvedEntry = options.resolveActionEntry?.(tool);
48
+ const { authorize, ...resolvedEntry } = options.resolveActionEntry?.(tool) ?? {};
49
49
  return {
50
50
  tool: {
51
51
  description: tool.description,
@@ -59,7 +59,7 @@ function mcpToolToActionEntry(manager, tool, options = {}) {
59
59
  },
60
60
  ...(tool.annotations?.readOnlyHint === true ? { readOnly: true } : {}),
61
61
  ...resolvedEntry,
62
- run: async (args) => {
62
+ run: async (args, context) => {
63
63
  // Defense-in-depth: even if a cross-scope MCP tool somehow makes it
64
64
  // into the LLM's visible tool list, reject invocation here so we never
65
65
  // execute a user's credentials on behalf of another user.
@@ -73,10 +73,16 @@ function mcpToolToActionEntry(manager, tool, options = {}) {
73
73
  }
74
74
  }
75
75
  try {
76
- const result = await manager.callTool(tool.name, args);
76
+ const result = await manager.callTool(tool.name, args, {
77
+ context,
78
+ authorize,
79
+ });
77
80
  return await buildMcpActionResult(manager, tool, args, result);
78
81
  }
79
82
  catch (err) {
83
+ if (err?.statusCode === 403) {
84
+ throw err;
85
+ }
80
86
  return buildMcpErrorActionResult(tool, args, `Error calling MCP tool ${tool.name}: ${err?.message ?? err}`);
81
87
  }
82
88
  },
@@ -1,3 +1,4 @@
1
+ import { type ActionAuthorize, type ActionRunContext } from "../action.js";
1
2
  import type { McpConfig, McpServerConfig } from "./config.js";
2
3
  export declare const MCP_TOOL_PREFIX = "mcp__";
3
4
  export interface McpTool {
@@ -21,6 +22,13 @@ export interface McpClientManagerOptions {
21
22
  debug?: boolean;
22
23
  connectTimeoutMs?: number;
23
24
  }
25
+ export interface McpToolCallOptions {
26
+ /** Identity and invocation source forwarded to the authorization callback. */
27
+ context?: ActionRunContext;
28
+ /** Fallback used by standalone action wrappers without a manager resolver. */
29
+ authorize?: ActionAuthorize<Record<string, unknown>>;
30
+ }
31
+ export type McpToolAuthorizationResolver = (tool: McpTool) => ActionAuthorize<Record<string, unknown>> | undefined;
24
32
  export declare class McpClientManager {
25
33
  private readonly servers;
26
34
  private readonly debug;
@@ -29,8 +37,15 @@ export declare class McpClientManager {
29
37
  private config;
30
38
  private sdk;
31
39
  private readonly listeners;
40
+ private resolveToolAuthorization?;
32
41
  private reconfigureQueue;
33
42
  constructor(config: McpConfig | null, options?: McpClientManagerOptions);
43
+ /**
44
+ * Install the app-owned authorization resolver used by every MCP tool-call
45
+ * surface. Passing `undefined` restores the backwards-compatible no-policy
46
+ * behavior.
47
+ */
48
+ setToolAuthorizationResolver(resolver?: McpToolAuthorizationResolver): void;
34
49
  get enabled(): boolean;
35
50
  getConfig(): McpConfig | null;
36
51
  get configuredServers(): string[];
@@ -69,7 +84,7 @@ export declare class McpClientManager {
69
84
  /** The config the server's current tools were loaded from. */
70
85
  getServerConfig(serverId: string): McpServerConfig | null;
71
86
  hasServer(serverId: string): boolean;
72
- callTool(prefixedName: string, args: unknown): Promise<unknown>;
87
+ callTool(prefixedName: string, args: unknown, options?: McpToolCallOptions): Promise<unknown>;
73
88
  readResource(serverId: string, uri: string): Promise<unknown>;
74
89
  readResourceForTool(prefixedName: string, uri: string): Promise<unknown>;
75
90
  stop(): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE } from "../action.js";
1
+ import { MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, } from "../action.js";
2
2
  import { formatMcpConnectError, httpStatusFromError } from "./errors.js";
3
3
  import { isFirstPartyRemoteEndpointTrusted, parseMergedKey, } from "./remote-store.js";
4
4
  export const MCP_TOOL_PREFIX = "mcp__";
@@ -110,12 +110,21 @@ export class McpClientManager {
110
110
  config;
111
111
  sdk = null;
112
112
  listeners = new Set();
113
+ resolveToolAuthorization;
113
114
  reconfigureQueue = Promise.resolve();
114
115
  constructor(config, options = {}) {
115
116
  this.config = config;
116
117
  this.debug = !!options.debug;
117
118
  this.connectTimeoutMs = options.connectTimeoutMs;
118
119
  }
120
+ /**
121
+ * Install the app-owned authorization resolver used by every MCP tool-call
122
+ * surface. Passing `undefined` restores the backwards-compatible no-policy
123
+ * behavior.
124
+ */
125
+ setToolAuthorizationResolver(resolver) {
126
+ this.resolveToolAuthorization = resolver;
127
+ }
119
128
  get enabled() {
120
129
  return !!this.config && Object.keys(this.config.servers).length > 0;
121
130
  }
@@ -546,7 +555,7 @@ export class McpClientManager {
546
555
  const entry = this.servers.get(serverId);
547
556
  return !!entry?.client && !entry.error;
548
557
  }
549
- async callTool(prefixedName, args) {
558
+ async callTool(prefixedName, args, options = {}) {
550
559
  const parsed = parseMcpToolName(prefixedName);
551
560
  if (!parsed) {
552
561
  throw new Error(`Tool name "${prefixedName}" does not look like an MCP tool (expected mcp__<server>__<tool>)`);
@@ -555,16 +564,35 @@ export class McpClientManager {
555
564
  if (!entry || !entry.client) {
556
565
  throw new Error(`MCP server "${parsed.serverId}" is not connected${entry?.error ? `: ${entry.error}` : ""}`);
557
566
  }
558
- return this.withExpiredSessionRetry(entry, (current) => {
567
+ const normalizedArgs = args && typeof args === "object" ? args : {};
568
+ // The expired-session retry replays this attempt on a reconnected entry
569
+ // whose tool metadata was reloaded, so authorization must run per attempt.
570
+ return this.withExpiredSessionRetry(entry, async (current) => {
559
571
  const known = current.tools.find((t) => t.name === prefixedName);
560
572
  if (!known) {
561
573
  throw new Error(`MCP server "${parsed.serverId}" does not expose tool "${parsed.toolName}"`);
562
574
  }
563
- return current.client.callTool({
575
+ const client = current.client;
576
+ const authorize = this.resolveToolAuthorization?.(known) ?? options.authorize;
577
+ if (authorize) {
578
+ const verdict = await authorize(normalizedArgs, options.context);
579
+ if (verdict === false) {
580
+ const error = new Error("Not authorized");
581
+ error.name = "ForbiddenError";
582
+ error.statusCode = 403;
583
+ throw error;
584
+ }
585
+ }
586
+ const liveEntry = this.servers.get(parsed.serverId);
587
+ const liveTool = liveEntry?.tools.find((tool) => tool.name === prefixedName);
588
+ if (liveEntry !== current ||
589
+ liveEntry.client !== client ||
590
+ liveTool !== known) {
591
+ throw new Error(`MCP server "${parsed.serverId}" changed while authorizing tool "${parsed.toolName}"`);
592
+ }
593
+ return client.callTool({
564
594
  name: parsed.toolName,
565
- arguments: args && typeof args === "object"
566
- ? args
567
- : {},
595
+ arguments: normalizedArgs,
568
596
  });
569
597
  });
570
598
  }
@@ -404,10 +404,15 @@ async function withMcpAppRequestContext(event, fn) {
404
404
  setResponseStatus(event, 401);
405
405
  return { error: "Authentication required" };
406
406
  }
407
+ const context = {
408
+ caller: "frontend",
409
+ userEmail: email,
410
+ orgId: orgId ?? null,
411
+ };
407
412
  return runWithRequestContext({
408
413
  userEmail: email ?? undefined,
409
414
  orgId: orgId ?? undefined,
410
- }, fn);
415
+ }, () => fn(context));
411
416
  }
412
417
  function serverHasVisibleTools(manager, serverId) {
413
418
  return manager
@@ -465,7 +470,7 @@ async function handleMcpAppCallTool(event, manager) {
465
470
  setResponseStatus(event, 400);
466
471
  return { error: "serverId and same-server toolName are required" };
467
472
  }
468
- return withMcpAppRequestContext(event, async () => {
473
+ return withMcpAppRequestContext(event, async (context) => {
469
474
  const tool = mcpAppCallableTool(manager, serverId, originalToolName);
470
475
  if (!tool) {
471
476
  setResponseStatus(event, 403);
@@ -474,10 +479,10 @@ async function handleMcpAppCallTool(event, manager) {
474
479
  try {
475
480
  return await manager.callTool(tool.name, body.arguments && typeof body.arguments === "object"
476
481
  ? body.arguments
477
- : {});
482
+ : {}, { context });
478
483
  }
479
484
  catch (err) {
480
- setResponseStatus(event, 400);
485
+ setResponseStatus(event, err?.statusCode === 403 ? 403 : 400);
481
486
  return { error: err?.message ?? "MCP tool call failed" };
482
487
  }
483
488
  });
@@ -27,6 +27,8 @@ export interface GenAiChatMetric {
27
27
  supportedModels?: readonly string[];
28
28
  durationMs?: number;
29
29
  failed?: boolean;
30
+ /** The engine's error code for a failed call, bounded before it becomes `error.type`. */
31
+ errorCode?: string;
30
32
  inputTokens?: number;
31
33
  outputTokens?: number;
32
34
  }
@@ -7,6 +7,7 @@
7
7
  * a migration plan.
8
8
  */
9
9
  import { CORE_ACTION_GROUPS } from "../framework-tools.js";
10
+ import { agentTroubleCauseForCode } from "../shared/analytics-events.js";
10
11
  import { getRegisteredObservabilityProvider, } from "./otel-provider.js";
11
12
  import { trackingIdentityProperties } from "./tracking-identity.js";
12
13
  const METER_NAME = "@agent-native/core";
@@ -120,6 +121,14 @@ function boundedModel(model, supportedModels) {
120
121
  ? model
121
122
  : "_OTHER";
122
123
  }
124
+ // Error codes are thrower-supplied strings, so only HTTP statuses and the
125
+ // closed trouble-cause set reach the label; everything else stays `_OTHER`.
126
+ function boundedGenAiErrorType(errorCode) {
127
+ const normalized = errorCode?.trim().toLowerCase();
128
+ if (normalized && /^http_[45]\d\d$/.test(normalized))
129
+ return normalized;
130
+ return agentTroubleCauseForCode(normalized) ?? "_OTHER";
131
+ }
123
132
  /** One model round trip (`gen_ai.operation.name=chat`). */
124
133
  export function recordGenAiChat(call) {
125
134
  const recorded = instruments();
@@ -131,7 +140,12 @@ export function recordGenAiChat(call) {
131
140
  ...(call.providerName ? { "gen_ai.provider.name": call.providerName } : {}),
132
141
  };
133
142
  if (call.durationMs !== undefined) {
134
- recorded.genAiOperationDuration.record(Math.max(0, call.durationMs) / 1_000, call.failed ? { ...attributes, "error.type": "_OTHER" } : attributes);
143
+ recorded.genAiOperationDuration.record(Math.max(0, call.durationMs) / 1_000, call.failed
144
+ ? {
145
+ ...attributes,
146
+ "error.type": boundedGenAiErrorType(call.errorCode),
147
+ }
148
+ : attributes);
135
149
  }
136
150
  for (const [tokenType, tokens] of [
137
151
  ["input", call.inputTokens],
@@ -523,7 +523,7 @@ export async function instrumentAgentLoop(opts) {
523
523
  finishOtelModelSpan(tripIndex, {
524
524
  status: modelCallFailed ? "error" : "success",
525
525
  errorMessage: modelErrorMessage,
526
- errorCode: finalErrorCode,
526
+ errorCode: trip?.errorCode ?? finalErrorCode,
527
527
  attributes: modelSpanAttributes(tripIndex),
528
528
  endTime: modelRoundTrips[tripIndex]?.end,
529
529
  });
@@ -646,6 +646,8 @@ export async function instrumentAgentLoop(opts) {
646
646
  trip.end = end;
647
647
  if (event.reason)
648
648
  trip.stopReason = event.reason;
649
+ if (event.errorCode)
650
+ trip.errorCode = event.errorCode;
649
651
  }
650
652
  if (event.reason === undefined || event.reason === "error") {
651
653
  modelSpansAwaitingFinalError.add(tripIndex);
@@ -1362,6 +1364,11 @@ export async function instrumentAgentLoop(opts) {
1362
1364
  failed: tripIndex === interruptedModelRoundTrip ||
1363
1365
  !trip.stopReason ||
1364
1366
  trip.stopReason === "error",
1367
+ // Only the last call's failure can be the one that ended the run.
1368
+ errorCode: trip.errorCode ??
1369
+ (tripIndex === modelRoundTrips.length - 1
1370
+ ? runTerminalCode
1371
+ : undefined),
1365
1372
  inputTokens: trip.usage?.inputTokens,
1366
1373
  outputTokens: trip.usage?.outputTokens,
1367
1374
  });
@@ -75,10 +75,10 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
+ id?: undefined;
78
79
  provider?: undefined;
79
80
  deleted?: undefined;
80
- id?: undefined;
81
- found?: undefined;
81
+ message?: undefined;
82
82
  providers: {
83
83
  id: string;
84
84
  label: string;
@@ -89,45 +89,45 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
89
89
  updatedAt: number;
90
90
  }[];
91
91
  count: number;
92
+ found?: undefined;
92
93
  registered?: undefined;
93
94
  label?: undefined;
94
- message?: undefined;
95
95
  } | {
96
- deleted?: undefined;
97
96
  id?: undefined;
98
97
  count?: undefined;
98
+ deleted?: undefined;
99
+ message?: undefined;
99
100
  providers?: undefined;
100
101
  found: boolean;
101
102
  provider: import("../custom-registry.js").CustomProviderConfig;
102
103
  registered?: undefined;
103
104
  label?: undefined;
104
- message?: undefined;
105
105
  } | {
106
106
  provider?: undefined;
107
- deleted?: undefined;
108
107
  count?: undefined;
108
+ deleted?: undefined;
109
+ message?: undefined;
109
110
  providers?: undefined;
110
111
  found: boolean;
111
112
  id: string;
112
113
  registered?: undefined;
113
114
  label?: undefined;
114
- message?: undefined;
115
115
  } | {
116
116
  provider?: undefined;
117
117
  count?: undefined;
118
- found?: undefined;
118
+ message?: undefined;
119
119
  providers?: undefined;
120
+ found?: undefined;
120
121
  deleted: boolean;
121
122
  id: string;
122
123
  registered?: undefined;
123
124
  label?: undefined;
124
- message?: undefined;
125
125
  } | {
126
126
  provider?: undefined;
127
- deleted?: undefined;
128
127
  count?: undefined;
129
- found?: undefined;
128
+ deleted?: undefined;
130
129
  providers?: undefined;
130
+ found?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
133
133
  label: string;