@agent-native/core 0.17.2 → 0.18.1

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 (80) hide show
  1. package/dist/action.d.ts +27 -0
  2. package/dist/action.d.ts.map +1 -1
  3. package/dist/action.js +2 -0
  4. package/dist/action.js.map +1 -1
  5. package/dist/agent/production-agent.d.ts +4 -0
  6. package/dist/agent/production-agent.d.ts.map +1 -1
  7. package/dist/agent/production-agent.js.map +1 -1
  8. package/dist/cli/index.js +16 -0
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/mcp.d.ts +16 -0
  11. package/dist/cli/mcp.d.ts.map +1 -0
  12. package/dist/cli/mcp.js +583 -0
  13. package/dist/cli/mcp.js.map +1 -0
  14. package/dist/db/client.d.ts +17 -14
  15. package/dist/db/client.d.ts.map +1 -1
  16. package/dist/db/client.js +31 -27
  17. package/dist/db/client.js.map +1 -1
  18. package/dist/db/create-get-db.js +2 -2
  19. package/dist/db/create-get-db.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/mcp/build-server.d.ts +152 -0
  24. package/dist/mcp/build-server.d.ts.map +1 -0
  25. package/dist/mcp/build-server.js +349 -0
  26. package/dist/mcp/build-server.js.map +1 -0
  27. package/dist/mcp/builtin-tools.d.ts +39 -0
  28. package/dist/mcp/builtin-tools.d.ts.map +1 -0
  29. package/dist/mcp/builtin-tools.js +401 -0
  30. package/dist/mcp/builtin-tools.js.map +1 -0
  31. package/dist/mcp/index.d.ts +7 -0
  32. package/dist/mcp/index.d.ts.map +1 -1
  33. package/dist/mcp/index.js +8 -0
  34. package/dist/mcp/index.js.map +1 -1
  35. package/dist/mcp/server.d.ts +3 -13
  36. package/dist/mcp/server.d.ts.map +1 -1
  37. package/dist/mcp/server.js +44 -179
  38. package/dist/mcp/server.js.map +1 -1
  39. package/dist/mcp/stdio.d.ts +44 -0
  40. package/dist/mcp/stdio.d.ts.map +1 -0
  41. package/dist/mcp/stdio.js +209 -0
  42. package/dist/mcp/stdio.js.map +1 -0
  43. package/dist/mcp/workspace-resolve.d.ts +68 -0
  44. package/dist/mcp/workspace-resolve.d.ts.map +1 -0
  45. package/dist/mcp/workspace-resolve.js +205 -0
  46. package/dist/mcp/workspace-resolve.js.map +1 -0
  47. package/dist/server/action-discovery.d.ts.map +1 -1
  48. package/dist/server/action-discovery.js +3 -0
  49. package/dist/server/action-discovery.js.map +1 -1
  50. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  51. package/dist/server/agent-chat-plugin.js +1 -0
  52. package/dist/server/agent-chat-plugin.js.map +1 -1
  53. package/dist/server/auth.d.ts +9 -0
  54. package/dist/server/auth.d.ts.map +1 -1
  55. package/dist/server/auth.js +71 -19
  56. package/dist/server/auth.js.map +1 -1
  57. package/dist/server/better-auth-instance.d.ts.map +1 -1
  58. package/dist/server/better-auth-instance.js +15 -10
  59. package/dist/server/better-auth-instance.js.map +1 -1
  60. package/dist/server/core-routes-plugin.d.ts +5 -0
  61. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  62. package/dist/server/core-routes-plugin.js +9 -0
  63. package/dist/server/core-routes-plugin.js.map +1 -1
  64. package/dist/server/deep-link.d.ts +55 -0
  65. package/dist/server/deep-link.d.ts.map +1 -0
  66. package/dist/server/deep-link.js +69 -0
  67. package/dist/server/deep-link.js.map +1 -0
  68. package/dist/server/index.d.ts +2 -0
  69. package/dist/server/index.d.ts.map +1 -1
  70. package/dist/server/index.js +2 -0
  71. package/dist/server/index.js.map +1 -1
  72. package/dist/server/open-route.d.ts +12 -0
  73. package/dist/server/open-route.d.ts.map +1 -0
  74. package/dist/server/open-route.js +159 -0
  75. package/dist/server/open-route.js.map +1 -0
  76. package/dist/server/request-context.d.ts +8 -0
  77. package/dist/server/request-context.d.ts.map +1 -1
  78. package/dist/server/request-context.js.map +1 -1
  79. package/docs/content/external-agents.md +177 -0
  80. package/package.json +1 -1
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Shared MCP server builder.
3
+ *
4
+ * Extracted from `server.ts` so the stateless Streamable-HTTP mount
5
+ * (`mountMCP`) and the stdio transport (`runMCPStdio --standalone`) build the
6
+ * *same* MCP server from the *same* `ActionEntry` registry. Both surfaces:
7
+ *
8
+ * - expose every action as an MCP tool (+ the `ask-agent` meta-tool),
9
+ * - append the framework deep-link block / `_meta` to every tool result,
10
+ * - wrap `run()` / `askAgent()` in `runWithRequestContext` so per-user /
11
+ * per-org scoping (accessFilter, resolveCredential, MCP visibility) is
12
+ * honoured.
13
+ *
14
+ * `server.ts` re-exports `createMCPServerForRequest` and the auth helpers so
15
+ * any (future) external importer of `@agent-native/core/mcp` keeps resolving.
16
+ *
17
+ * Node-only at the SDK level, but this module itself has no Node-only imports
18
+ * — it can be bundled into the serverless function alongside `mountMCP`.
19
+ */
20
+ import type { ActionEntry } from "../agent/production-agent.js";
21
+ export interface MCPConfig {
22
+ /** App name shown in MCP server info */
23
+ name: string;
24
+ /**
25
+ * Canonical app id (directory under `apps/`, e.g. `mail`) this MCP server
26
+ * is mounted for. Optional & back-compat: when omitted the builtin
27
+ * cross-app tools fall back to lowercasing `name`. Used by `open_app` /
28
+ * `ask_app` / `create_workspace_app` to tell "this app" from a cross-app
29
+ * target so they resolve the *target* app's origin rather than echoing the
30
+ * current request origin.
31
+ */
32
+ appId?: string;
33
+ /** App description */
34
+ description: string;
35
+ /** Version string (default "1.0.0") */
36
+ version?: string;
37
+ /** Action registry — same as agent chat and A2A */
38
+ actions: Record<string, ActionEntry>;
39
+ /** Handler for the ask-agent meta-tool — runs the full agent loop */
40
+ askAgent?: (message: string) => Promise<string>;
41
+ /**
42
+ * Disable the generic cross-app builtin tools (`list_apps`, `open_app`,
43
+ * `ask_app`, `create_workspace_app`, `list_templates`). They are merged in
44
+ * by default so external agents get a stable verb set; a template action of
45
+ * the same name always wins (template precedence). Set to `false` only for
46
+ * a constrained / locked-down mount.
47
+ */
48
+ builtinCrossAppTools?: boolean;
49
+ }
50
+ /**
51
+ * Identity extracted from a verified MCP bearer token / JWT. Used to wrap
52
+ * `entry.run()` and `config.askAgent()` calls in `runWithRequestContext`
53
+ * so downstream tools (db-query, accessFilter, resolveCredential) honour
54
+ * per-user / per-org scoping. Without this wrap the MCP endpoint would
55
+ * silently bypass tenant isolation. See finding #6 in
56
+ * /tmp/security-audit/12-mcp-a2a-agent.md.
57
+ */
58
+ export interface MCPCallerIdentity {
59
+ userEmail: string | undefined;
60
+ orgDomain: string | undefined;
61
+ }
62
+ /** Per-request context used to turn an action's relative deep link into the
63
+ * absolute web URL (and desktop `agentnative://` URL) the external agent
64
+ * surfaces. Derived from the inbound request headers in `mountMCP`, or from
65
+ * the resolved local app origin in the stdio standalone path. */
66
+ export interface MCPRequestMeta {
67
+ /** Origin of the running app, e.g. `http://localhost:8100`. */
68
+ origin?: string;
69
+ /** Optional client preference for which URL the *markdown* link uses. */
70
+ target?: "browser" | "desktop" | "terminal";
71
+ }
72
+ /**
73
+ * Build the deep-link content block + structured `_meta` for a tool result.
74
+ * Best-effort: any throw / nullish link is swallowed so a bad `link` builder
75
+ * never fails the tool call.
76
+ */
77
+ export declare function buildLinkArtifacts(entry: ActionEntry, args: Record<string, any>, result: any, meta: MCPRequestMeta | undefined): {
78
+ block?: {
79
+ type: "text";
80
+ text: string;
81
+ };
82
+ _meta?: Record<string, unknown>;
83
+ };
84
+ /**
85
+ * Build a fully-wired MCP `Server` for a single request / session.
86
+ *
87
+ * Shared by the stateless Streamable-HTTP mount (`mountMCP`) and the stdio
88
+ * standalone transport. The HTTP mount passes the per-request origin via
89
+ * `requestMeta`; the stdio standalone path passes the resolved local app
90
+ * origin so deep links still become absolute URLs.
91
+ */
92
+ export declare function createMCPServerForRequest(config: MCPConfig, identity: MCPCallerIdentity | undefined, requestMeta?: MCPRequestMeta): Promise<import("@modelcontextprotocol/sdk/server").Server<{
93
+ method: string;
94
+ params?: {
95
+ [x: string]: unknown;
96
+ _meta?: {
97
+ [x: string]: unknown;
98
+ progressToken?: string | number;
99
+ "io.modelcontextprotocol/related-task"?: {
100
+ taskId: string;
101
+ };
102
+ };
103
+ };
104
+ }, {
105
+ method: string;
106
+ params?: {
107
+ [x: string]: unknown;
108
+ _meta?: {
109
+ [x: string]: unknown;
110
+ progressToken?: string | number;
111
+ "io.modelcontextprotocol/related-task"?: {
112
+ taskId: string;
113
+ };
114
+ };
115
+ };
116
+ }, {
117
+ [x: string]: unknown;
118
+ _meta?: {
119
+ [x: string]: unknown;
120
+ progressToken?: string | number;
121
+ "io.modelcontextprotocol/related-task"?: {
122
+ taskId: string;
123
+ };
124
+ };
125
+ }>>;
126
+ export declare function getAccessTokens(): string[];
127
+ /**
128
+ * Verify the inbound auth header. Returns:
129
+ * - { authed: true, identity } when verified — `identity` is derived from
130
+ * the JWT (`sub` / `org_domain`) for JWT auth, or from the
131
+ * `AGENT_NATIVE_OWNER_EMAIL` env / `X-Agent-Native-Owner-Email` header
132
+ * for static-token auth (the `agent-native mcp install` flow). `identity`
133
+ * is undefined only for true dev-open with no owner hint.
134
+ * - { authed: false } on rejection.
135
+ *
136
+ * When A2A_SECRET is set we extract the JWT's `sub` (caller email) and
137
+ * `org_domain` claims so the MCP endpoint can wrap tool runs in
138
+ * `runWithRequestContext({ userEmail, orgId })`. Without that wrap, the
139
+ * MCP endpoint loses tenant identity and downstream `accessFilter` /
140
+ * `resolveCredential` calls fall back to platform-wide defaults.
141
+ *
142
+ * `ownerEmailHeader` is the forwarded `X-Agent-Native-Owner-Email` value; it
143
+ * is consulted ONLY on the static-token / dev-open path (never to influence
144
+ * verified JWT identity), so the install flow runs tools as the configured
145
+ * owner instead of an unscoped anonymous caller.
146
+ */
147
+ export declare function verifyAuth(authHeader: string | undefined, ownerEmailHeader?: string | undefined): Promise<{
148
+ authed: boolean;
149
+ identity?: MCPCallerIdentity;
150
+ }>;
151
+ export declare function resolveOrgIdFromDomain(orgDomain: string | undefined): Promise<string | undefined>;
152
+ //# sourceMappingURL=build-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-server.d.ts","sourceRoot":"","sources":["../../src/mcp/build-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAKhE,MAAM,WAAW,SAAS;IACxB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED;;;kEAGkE;AAClE,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;CAC7C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,MAAM,EAAE,GAAG,EACX,IAAI,EAAE,cAAc,GAAG,SAAS,GAC/B;IACD,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAsBA;AA2BD;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,WAAW,CAAC,EAAE,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8J7B;AAOD,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAc1C;AAyCD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,GACpC,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CAAE,CAAC,CAiD5D;AAED,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS7B"}
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Shared MCP server builder.
3
+ *
4
+ * Extracted from `server.ts` so the stateless Streamable-HTTP mount
5
+ * (`mountMCP`) and the stdio transport (`runMCPStdio --standalone`) build the
6
+ * *same* MCP server from the *same* `ActionEntry` registry. Both surfaces:
7
+ *
8
+ * - expose every action as an MCP tool (+ the `ask-agent` meta-tool),
9
+ * - append the framework deep-link block / `_meta` to every tool result,
10
+ * - wrap `run()` / `askAgent()` in `runWithRequestContext` so per-user /
11
+ * per-org scoping (accessFilter, resolveCredential, MCP visibility) is
12
+ * honoured.
13
+ *
14
+ * `server.ts` re-exports `createMCPServerForRequest` and the auth helpers so
15
+ * any (future) external importer of `@agent-native/core/mcp` keeps resolving.
16
+ *
17
+ * Node-only at the SDK level, but this module itself has no Node-only imports
18
+ * — it can be bundled into the serverless function alongside `mountMCP`.
19
+ */
20
+ import { runWithRequestContext } from "../server/request-context.js";
21
+ import { toAbsoluteOpenUrl, toDesktopOpenUrl } from "../server/deep-link.js";
22
+ import { getBuiltinCrossAppTools } from "./builtin-tools.js";
23
+ /**
24
+ * Build the deep-link content block + structured `_meta` for a tool result.
25
+ * Best-effort: any throw / nullish link is swallowed so a bad `link` builder
26
+ * never fails the tool call.
27
+ */
28
+ export function buildLinkArtifacts(entry, args, result, meta) {
29
+ if (typeof entry.link !== "function")
30
+ return {};
31
+ try {
32
+ const lk = entry.link({ args: args ?? {}, result });
33
+ if (!lk?.url)
34
+ return {};
35
+ const webUrl = toAbsoluteOpenUrl(lk.url, meta?.origin);
36
+ const desktopUrl = toDesktopOpenUrl(lk.url);
37
+ const markdownUrl = meta?.target === "desktop" ? desktopUrl : webUrl;
38
+ return {
39
+ block: { type: "text", text: `\n\n[${lk.label} →](${markdownUrl})` },
40
+ _meta: {
41
+ "agent-native/openLink": {
42
+ label: lk.label,
43
+ view: lk.view,
44
+ webUrl,
45
+ desktopUrl,
46
+ },
47
+ },
48
+ };
49
+ }
50
+ catch {
51
+ return {};
52
+ }
53
+ }
54
+ /**
55
+ * Merge the generic cross-app builtin tools into the config's action
56
+ * registry. **Template actions take precedence**: if a template defines an
57
+ * action with the same name as a builtin (e.g. its own `list_apps`), the
58
+ * template entry wins and the builtin is dropped. This mirrors the
59
+ * template-over-workspace-core precedence in `autoDiscoverActions`.
60
+ *
61
+ * The builtins are pure-ish navigators / scaffolders; they call back into the
62
+ * same `config.actions` / `config.askAgent` so there is no second agent loop.
63
+ */
64
+ function mergeBuiltinTools(config) {
65
+ if (config.builtinCrossAppTools === false)
66
+ return config.actions;
67
+ const builtins = getBuiltinCrossAppTools(config);
68
+ const merged = { ...builtins };
69
+ // Template / app actions overwrite same-named builtins.
70
+ for (const [name, entry] of Object.entries(config.actions)) {
71
+ merged[name] = entry;
72
+ }
73
+ return merged;
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // MCP Server creation — converts ActionEntry registry to MCP tools
77
+ // ---------------------------------------------------------------------------
78
+ /**
79
+ * Build a fully-wired MCP `Server` for a single request / session.
80
+ *
81
+ * Shared by the stateless Streamable-HTTP mount (`mountMCP`) and the stdio
82
+ * standalone transport. The HTTP mount passes the per-request origin via
83
+ * `requestMeta`; the stdio standalone path passes the resolved local app
84
+ * origin so deep links still become absolute URLs.
85
+ */
86
+ export async function createMCPServerForRequest(config, identity, requestMeta) {
87
+ const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
88
+ const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
89
+ const server = new Server({ name: config.name, version: config.version ?? "1.0.0" }, { capabilities: { tools: {} } });
90
+ // The action set the request handlers operate on = template actions +
91
+ // generic cross-app builtins (template wins on name collision).
92
+ const actions = mergeBuiltinTools(config);
93
+ // Resolve the effective caller identity. JWT / header-derived identity
94
+ // (passed by `mountMCP` via `verifyAuth`) wins. When the caller passed no
95
+ // identity — the stdio **standalone** path — fall back to the
96
+ // `AGENT_NATIVE_OWNER_EMAIL` env the `agent-native mcp install` flow writes
97
+ // into the `agent-native mcp serve` process env, so standalone tool runs are
98
+ // tenant-scoped to the configured owner instead of running unscoped. Stays
99
+ // undefined for true dev-open (no token, no secret, no owner) — behavior
100
+ // there is unchanged.
101
+ const ownerFromEnv = process.env.AGENT_NATIVE_OWNER_EMAIL?.trim();
102
+ const effectiveIdentity = identity ??
103
+ (ownerFromEnv
104
+ ? { userEmail: ownerFromEnv, orgDomain: undefined }
105
+ : undefined);
106
+ // Resolve orgId once per request (DB lookup) so subsequent wraps are
107
+ // synchronous. The caller identity may be undefined for true dev-open —
108
+ // in that case we run with no userEmail/orgId, which makes downstream
109
+ // tools that require per-user scope return empty results rather than
110
+ // cross-tenant data (the safe default).
111
+ const orgIdPromise = resolveOrgIdFromDomain(effectiveIdentity?.orgDomain);
112
+ /**
113
+ * Wrap a callback in
114
+ * `runWithRequestContext({ userEmail, orgId, requestOrigin }, fn)`.
115
+ * Both the tools/list and tools/call handlers go through this so
116
+ * downstream `accessFilter`, `resolveCredential`, and per-user MCP
117
+ * visibility checks see the verified caller's identity. `requestOrigin`
118
+ * is the live server origin derived from the inbound request (same value
119
+ * used to absolutize deep links) so actions that build fetchable URLs
120
+ * (e.g. design `export-coding-handoff`'s signed raw-code URL) resolve the
121
+ * correct local-workspace origin instead of a prod/localhost fallback.
122
+ */
123
+ async function withCallerContext(fn) {
124
+ const orgId = await orgIdPromise;
125
+ return runWithRequestContext({
126
+ userEmail: effectiveIdentity?.userEmail,
127
+ orgId,
128
+ ...(requestMeta?.origin ? { requestOrigin: requestMeta.origin } : {}),
129
+ }, fn);
130
+ }
131
+ // tools/list — return all actions + ask-agent meta-tool. Wrapped in the
132
+ // request context so per-user MCP visibility (mcp-client/visibility.ts)
133
+ // applies to the listing too.
134
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
135
+ return withCallerContext(async () => {
136
+ const tools = Object.entries(actions).map(([name, entry]) => {
137
+ const hasLink = typeof entry.link === "function";
138
+ const baseDescription = entry.tool.description ?? name;
139
+ return {
140
+ name,
141
+ description: hasLink
142
+ ? `${baseDescription} After calling, surface the returned "Open in … →" link to the user.`
143
+ : baseDescription,
144
+ inputSchema: entry.tool.parameters ?? {
145
+ type: "object",
146
+ properties: {},
147
+ },
148
+ ...(hasLink
149
+ ? { annotations: { "agent-native/producesOpenLink": true } }
150
+ : {}),
151
+ };
152
+ });
153
+ if (config.askAgent) {
154
+ tools.push({
155
+ name: "ask-agent",
156
+ description: "Send a natural-language message to the app's AI agent and get a response. " +
157
+ "Use this for complex, multi-step tasks that require the agent's reasoning " +
158
+ "and full context about the app.",
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ message: {
163
+ type: "string",
164
+ description: "The message to send to the agent",
165
+ },
166
+ },
167
+ required: ["message"],
168
+ },
169
+ });
170
+ }
171
+ return { tools };
172
+ });
173
+ });
174
+ // tools/call — dispatch to action registry or ask-agent. Wrapped in the
175
+ // request context so the action's `run(args)` and `askAgent()` execute
176
+ // with the verified caller's identity, not the platform default.
177
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
178
+ return withCallerContext(async () => {
179
+ const { name, arguments: args } = request.params;
180
+ if (name === "ask-agent" && config.askAgent) {
181
+ const message = args?.message ?? "";
182
+ try {
183
+ const result = await config.askAgent(message);
184
+ return { content: [{ type: "text", text: result }] };
185
+ }
186
+ catch (err) {
187
+ return {
188
+ content: [{ type: "text", text: `Error: ${err.message}` }],
189
+ isError: true,
190
+ };
191
+ }
192
+ }
193
+ const entry = actions[name];
194
+ if (!entry) {
195
+ return {
196
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
197
+ isError: true,
198
+ };
199
+ }
200
+ try {
201
+ const result = await entry.run(args ?? {});
202
+ const text = typeof result === "string" ? result : JSON.stringify(result);
203
+ const content = [{ type: "text", text }];
204
+ const { block, _meta } = buildLinkArtifacts(entry, args ?? {}, result, requestMeta);
205
+ if (block)
206
+ content.push(block);
207
+ return { content, ...(_meta ? { _meta } : {}) };
208
+ }
209
+ catch (err) {
210
+ return {
211
+ content: [{ type: "text", text: `Error: ${err.message}` }],
212
+ isError: true,
213
+ };
214
+ }
215
+ });
216
+ });
217
+ return server;
218
+ }
219
+ // ---------------------------------------------------------------------------
220
+ // Auth — reuses the same pattern as A2A (Bearer token or JWT). Shared so the
221
+ // HTTP mount and any stdio-side auth-aware helper resolve identity identically.
222
+ // ---------------------------------------------------------------------------
223
+ export function getAccessTokens() {
224
+ const single = process.env.ACCESS_TOKEN;
225
+ const multi = process.env.ACCESS_TOKENS;
226
+ const tokens = [];
227
+ if (single)
228
+ tokens.push(single);
229
+ if (multi) {
230
+ tokens.push(...multi
231
+ .split(",")
232
+ .map((t) => t.trim())
233
+ .filter(Boolean));
234
+ }
235
+ return tokens;
236
+ }
237
+ /**
238
+ * Resolve the caller identity for a static-token (or dev-open) auth path.
239
+ *
240
+ * Static `ACCESS_TOKEN` / `ACCESS_TOKENS` auth carries no per-caller claims,
241
+ * so without this the MCP endpoint would run every tool with
242
+ * `userEmail === undefined` and per-user / per-org scoped actions
243
+ * (`accessFilter`, `resolveAccess`, `resolveCredential`) would return
244
+ * empty / wrong data. The `agent-native mcp install` flow writes
245
+ * `AGENT_NATIVE_OWNER_EMAIL` into the client config env and the stdio proxy
246
+ * forwards it as the `X-Agent-Native-Owner-Email` request header (see
247
+ * `mcp/stdio.ts#authHeaders`). We trust that owner hint *only* on the
248
+ * static-token path — JWT auth already carries a cryptographically verified
249
+ * `sub`, so the header is ignored there and never widens JWT scope.
250
+ *
251
+ * Precedence is server-trusted-first: the server process's
252
+ * `AGENT_NATIVE_OWNER_EMAIL` env (set out-of-band by the operator / deploy)
253
+ * ALWAYS wins, and a client-supplied `X-Agent-Native-Owner-Email` header is
254
+ * honored *only as a fallback when that env is unset*. A static `ACCESS_TOKEN`
255
+ * is a shared bearer secret; letting a request header override a
256
+ * server-configured owner would let anyone holding a leaked token act as any
257
+ * user. The header path remains for the single-tenant local-dev install flow
258
+ * where the app server process has no owner env and the token *is* the
259
+ * workspace secret; multi-tenant deployments must use A2A JWT (verified `sub`),
260
+ * not a static token, for per-user scope.
261
+ *
262
+ * Returns `undefined` when no owner email is available (true dev-open: no
263
+ * token, no secret, no owner) so behavior there stays unchanged.
264
+ */
265
+ function deriveStaticTokenIdentity(ownerEmailHeader) {
266
+ const owner = process.env.AGENT_NATIVE_OWNER_EMAIL?.trim() ||
267
+ (typeof ownerEmailHeader === "string" && ownerEmailHeader.trim()) ||
268
+ "";
269
+ if (!owner)
270
+ return undefined;
271
+ return { userEmail: owner, orgDomain: undefined };
272
+ }
273
+ /**
274
+ * Verify the inbound auth header. Returns:
275
+ * - { authed: true, identity } when verified — `identity` is derived from
276
+ * the JWT (`sub` / `org_domain`) for JWT auth, or from the
277
+ * `AGENT_NATIVE_OWNER_EMAIL` env / `X-Agent-Native-Owner-Email` header
278
+ * for static-token auth (the `agent-native mcp install` flow). `identity`
279
+ * is undefined only for true dev-open with no owner hint.
280
+ * - { authed: false } on rejection.
281
+ *
282
+ * When A2A_SECRET is set we extract the JWT's `sub` (caller email) and
283
+ * `org_domain` claims so the MCP endpoint can wrap tool runs in
284
+ * `runWithRequestContext({ userEmail, orgId })`. Without that wrap, the
285
+ * MCP endpoint loses tenant identity and downstream `accessFilter` /
286
+ * `resolveCredential` calls fall back to platform-wide defaults.
287
+ *
288
+ * `ownerEmailHeader` is the forwarded `X-Agent-Native-Owner-Email` value; it
289
+ * is consulted ONLY on the static-token / dev-open path (never to influence
290
+ * verified JWT identity), so the install flow runs tools as the configured
291
+ * owner instead of an unscoped anonymous caller.
292
+ */
293
+ export async function verifyAuth(authHeader, ownerEmailHeader) {
294
+ // No auth configured → allow (dev mode). Still honour an owner hint
295
+ // (env or forwarded header) so the install flow stays tenant-scoped.
296
+ const accessTokens = getAccessTokens();
297
+ const hasA2ASecret = !!process.env.A2A_SECRET;
298
+ if (accessTokens.length === 0 && !hasA2ASecret) {
299
+ return {
300
+ authed: true,
301
+ identity: deriveStaticTokenIdentity(ownerEmailHeader),
302
+ };
303
+ }
304
+ if (!authHeader?.startsWith("Bearer "))
305
+ return { authed: false };
306
+ const token = authHeader.slice(7);
307
+ // Try JWT via A2A_SECRET
308
+ if (hasA2ASecret) {
309
+ try {
310
+ const jose = await import("jose");
311
+ const { payload } = await jose.jwtVerify(token, new TextEncoder().encode(process.env.A2A_SECRET));
312
+ return {
313
+ authed: true,
314
+ identity: {
315
+ userEmail: typeof payload.sub === "string" ? payload.sub : undefined,
316
+ orgDomain: typeof payload.org_domain === "string"
317
+ ? payload.org_domain
318
+ : undefined,
319
+ },
320
+ };
321
+ }
322
+ catch {
323
+ // Not a valid JWT — fall through to token check
324
+ }
325
+ }
326
+ // Try ACCESS_TOKEN / ACCESS_TOKENS exact match. Static tokens carry no
327
+ // per-caller claims, so derive identity from the forwarded owner-email
328
+ // hint (install flow) — otherwise tools would run unscoped.
329
+ if (accessTokens.length > 0 && accessTokens.includes(token)) {
330
+ return {
331
+ authed: true,
332
+ identity: deriveStaticTokenIdentity(ownerEmailHeader),
333
+ };
334
+ }
335
+ return { authed: false };
336
+ }
337
+ export async function resolveOrgIdFromDomain(orgDomain) {
338
+ if (!orgDomain)
339
+ return undefined;
340
+ try {
341
+ const { resolveOrgByDomain } = await import("../org/context.js");
342
+ const org = await resolveOrgByDomain(orgDomain);
343
+ return org?.orgId ?? undefined;
344
+ }
345
+ catch {
346
+ return undefined;
347
+ }
348
+ }
349
+ //# sourceMappingURL=build-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-server.js","sourceRoot":"","sources":["../../src/mcp/build-server.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared MCP server builder.\n *\n * Extracted from `server.ts` so the stateless Streamable-HTTP mount\n * (`mountMCP`) and the stdio transport (`runMCPStdio --standalone`) build the\n * *same* MCP server from the *same* `ActionEntry` registry. Both surfaces:\n *\n * - expose every action as an MCP tool (+ the `ask-agent` meta-tool),\n * - append the framework deep-link block / `_meta` to every tool result,\n * - wrap `run()` / `askAgent()` in `runWithRequestContext` so per-user /\n * per-org scoping (accessFilter, resolveCredential, MCP visibility) is\n * honoured.\n *\n * `server.ts` re-exports `createMCPServerForRequest` and the auth helpers so\n * any (future) external importer of `@agent-native/core/mcp` keeps resolving.\n *\n * Node-only at the SDK level, but this module itself has no Node-only imports\n * — it can be bundled into the serverless function alongside `mountMCP`.\n */\n\nimport type { ActionEntry } from \"../agent/production-agent.js\";\nimport { runWithRequestContext } from \"../server/request-context.js\";\nimport { toAbsoluteOpenUrl, toDesktopOpenUrl } from \"../server/deep-link.js\";\nimport { getBuiltinCrossAppTools } from \"./builtin-tools.js\";\n\nexport interface MCPConfig {\n /** App name shown in MCP server info */\n name: string;\n /**\n * Canonical app id (directory under `apps/`, e.g. `mail`) this MCP server\n * is mounted for. Optional & back-compat: when omitted the builtin\n * cross-app tools fall back to lowercasing `name`. Used by `open_app` /\n * `ask_app` / `create_workspace_app` to tell \"this app\" from a cross-app\n * target so they resolve the *target* app's origin rather than echoing the\n * current request origin.\n */\n appId?: string;\n /** App description */\n description: string;\n /** Version string (default \"1.0.0\") */\n version?: string;\n /** Action registry — same as agent chat and A2A */\n actions: Record<string, ActionEntry>;\n /** Handler for the ask-agent meta-tool — runs the full agent loop */\n askAgent?: (message: string) => Promise<string>;\n /**\n * Disable the generic cross-app builtin tools (`list_apps`, `open_app`,\n * `ask_app`, `create_workspace_app`, `list_templates`). They are merged in\n * by default so external agents get a stable verb set; a template action of\n * the same name always wins (template precedence). Set to `false` only for\n * a constrained / locked-down mount.\n */\n builtinCrossAppTools?: boolean;\n}\n\n/**\n * Identity extracted from a verified MCP bearer token / JWT. Used to wrap\n * `entry.run()` and `config.askAgent()` calls in `runWithRequestContext`\n * so downstream tools (db-query, accessFilter, resolveCredential) honour\n * per-user / per-org scoping. Without this wrap the MCP endpoint would\n * silently bypass tenant isolation. See finding #6 in\n * /tmp/security-audit/12-mcp-a2a-agent.md.\n */\nexport interface MCPCallerIdentity {\n userEmail: string | undefined;\n orgDomain: string | undefined;\n}\n\n/** Per-request context used to turn an action's relative deep link into the\n * absolute web URL (and desktop `agentnative://` URL) the external agent\n * surfaces. Derived from the inbound request headers in `mountMCP`, or from\n * the resolved local app origin in the stdio standalone path. */\nexport interface MCPRequestMeta {\n /** Origin of the running app, e.g. `http://localhost:8100`. */\n origin?: string;\n /** Optional client preference for which URL the *markdown* link uses. */\n target?: \"browser\" | \"desktop\" | \"terminal\";\n}\n\n/**\n * Build the deep-link content block + structured `_meta` for a tool result.\n * Best-effort: any throw / nullish link is swallowed so a bad `link` builder\n * never fails the tool call.\n */\nexport function buildLinkArtifacts(\n entry: ActionEntry,\n args: Record<string, any>,\n result: any,\n meta: MCPRequestMeta | undefined,\n): {\n block?: { type: \"text\"; text: string };\n _meta?: Record<string, unknown>;\n} {\n if (typeof entry.link !== \"function\") return {};\n try {\n const lk = entry.link({ args: args ?? {}, result });\n if (!lk?.url) return {};\n const webUrl = toAbsoluteOpenUrl(lk.url, meta?.origin);\n const desktopUrl = toDesktopOpenUrl(lk.url);\n const markdownUrl = meta?.target === \"desktop\" ? desktopUrl : webUrl;\n return {\n block: { type: \"text\", text: `\\n\\n[${lk.label} →](${markdownUrl})` },\n _meta: {\n \"agent-native/openLink\": {\n label: lk.label,\n view: lk.view,\n webUrl,\n desktopUrl,\n },\n },\n };\n } catch {\n return {};\n }\n}\n\n/**\n * Merge the generic cross-app builtin tools into the config's action\n * registry. **Template actions take precedence**: if a template defines an\n * action with the same name as a builtin (e.g. its own `list_apps`), the\n * template entry wins and the builtin is dropped. This mirrors the\n * template-over-workspace-core precedence in `autoDiscoverActions`.\n *\n * The builtins are pure-ish navigators / scaffolders; they call back into the\n * same `config.actions` / `config.askAgent` so there is no second agent loop.\n */\nfunction mergeBuiltinTools(config: MCPConfig): Record<string, ActionEntry> {\n if (config.builtinCrossAppTools === false) return config.actions;\n const builtins = getBuiltinCrossAppTools(config);\n const merged: Record<string, ActionEntry> = { ...builtins };\n // Template / app actions overwrite same-named builtins.\n for (const [name, entry] of Object.entries(config.actions)) {\n merged[name] = entry;\n }\n return merged;\n}\n\n// ---------------------------------------------------------------------------\n// MCP Server creation — converts ActionEntry registry to MCP tools\n// ---------------------------------------------------------------------------\n\n/**\n * Build a fully-wired MCP `Server` for a single request / session.\n *\n * Shared by the stateless Streamable-HTTP mount (`mountMCP`) and the stdio\n * standalone transport. The HTTP mount passes the per-request origin via\n * `requestMeta`; the stdio standalone path passes the resolved local app\n * origin so deep links still become absolute URLs.\n */\nexport async function createMCPServerForRequest(\n config: MCPConfig,\n identity: MCPCallerIdentity | undefined,\n requestMeta?: MCPRequestMeta,\n) {\n const { Server } = await import(\"@modelcontextprotocol/sdk/server/index.js\");\n const { ListToolsRequestSchema, CallToolRequestSchema } =\n await import(\"@modelcontextprotocol/sdk/types.js\");\n\n const server = new Server(\n { name: config.name, version: config.version ?? \"1.0.0\" },\n { capabilities: { tools: {} } },\n );\n\n // The action set the request handlers operate on = template actions +\n // generic cross-app builtins (template wins on name collision).\n const actions = mergeBuiltinTools(config);\n\n // Resolve the effective caller identity. JWT / header-derived identity\n // (passed by `mountMCP` via `verifyAuth`) wins. When the caller passed no\n // identity — the stdio **standalone** path — fall back to the\n // `AGENT_NATIVE_OWNER_EMAIL` env the `agent-native mcp install` flow writes\n // into the `agent-native mcp serve` process env, so standalone tool runs are\n // tenant-scoped to the configured owner instead of running unscoped. Stays\n // undefined for true dev-open (no token, no secret, no owner) — behavior\n // there is unchanged.\n const ownerFromEnv = process.env.AGENT_NATIVE_OWNER_EMAIL?.trim();\n const effectiveIdentity: MCPCallerIdentity | undefined =\n identity ??\n (ownerFromEnv\n ? { userEmail: ownerFromEnv, orgDomain: undefined }\n : undefined);\n\n // Resolve orgId once per request (DB lookup) so subsequent wraps are\n // synchronous. The caller identity may be undefined for true dev-open —\n // in that case we run with no userEmail/orgId, which makes downstream\n // tools that require per-user scope return empty results rather than\n // cross-tenant data (the safe default).\n const orgIdPromise = resolveOrgIdFromDomain(effectiveIdentity?.orgDomain);\n\n /**\n * Wrap a callback in\n * `runWithRequestContext({ userEmail, orgId, requestOrigin }, fn)`.\n * Both the tools/list and tools/call handlers go through this so\n * downstream `accessFilter`, `resolveCredential`, and per-user MCP\n * visibility checks see the verified caller's identity. `requestOrigin`\n * is the live server origin derived from the inbound request (same value\n * used to absolutize deep links) so actions that build fetchable URLs\n * (e.g. design `export-coding-handoff`'s signed raw-code URL) resolve the\n * correct local-workspace origin instead of a prod/localhost fallback.\n */\n async function withCallerContext<T>(fn: () => Promise<T>): Promise<T> {\n const orgId = await orgIdPromise;\n return runWithRequestContext(\n {\n userEmail: effectiveIdentity?.userEmail,\n orgId,\n ...(requestMeta?.origin ? { requestOrigin: requestMeta.origin } : {}),\n },\n fn,\n ) as Promise<T>;\n }\n\n // tools/list — return all actions + ask-agent meta-tool. Wrapped in the\n // request context so per-user MCP visibility (mcp-client/visibility.ts)\n // applies to the listing too.\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return withCallerContext(async () => {\n const tools = Object.entries(actions).map(([name, entry]) => {\n const hasLink = typeof entry.link === \"function\";\n const baseDescription = entry.tool.description ?? name;\n return {\n name,\n description: hasLink\n ? `${baseDescription} After calling, surface the returned \"Open in … →\" link to the user.`\n : baseDescription,\n inputSchema: entry.tool.parameters ?? {\n type: \"object\" as const,\n properties: {},\n },\n ...(hasLink\n ? { annotations: { \"agent-native/producesOpenLink\": true } }\n : {}),\n };\n });\n\n if (config.askAgent) {\n tools.push({\n name: \"ask-agent\",\n description:\n \"Send a natural-language message to the app's AI agent and get a response. \" +\n \"Use this for complex, multi-step tasks that require the agent's reasoning \" +\n \"and full context about the app.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n message: {\n type: \"string\",\n description: \"The message to send to the agent\",\n },\n },\n required: [\"message\"],\n },\n });\n }\n\n return { tools };\n });\n });\n\n // tools/call — dispatch to action registry or ask-agent. Wrapped in the\n // request context so the action's `run(args)` and `askAgent()` execute\n // with the verified caller's identity, not the platform default.\n server.setRequestHandler(CallToolRequestSchema, async (request: any) => {\n return withCallerContext(async () => {\n const { name, arguments: args } = request.params;\n\n if (name === \"ask-agent\" && config.askAgent) {\n const message = args?.message ?? \"\";\n try {\n const result = await config.askAgent(message);\n return { content: [{ type: \"text\", text: result }] };\n } catch (err: any) {\n return {\n content: [{ type: \"text\", text: `Error: ${err.message}` }],\n isError: true,\n };\n }\n }\n\n const entry = actions[name];\n if (!entry) {\n return {\n content: [{ type: \"text\", text: `Unknown tool: ${name}` }],\n isError: true,\n };\n }\n\n try {\n const result = await entry.run((args as Record<string, string>) ?? {});\n const text =\n typeof result === \"string\" ? result : JSON.stringify(result);\n const content: any[] = [{ type: \"text\", text }];\n const { block, _meta } = buildLinkArtifacts(\n entry,\n (args as Record<string, any>) ?? {},\n result,\n requestMeta,\n );\n if (block) content.push(block);\n return { content, ...(_meta ? { _meta } : {}) };\n } catch (err: any) {\n return {\n content: [{ type: \"text\", text: `Error: ${err.message}` }],\n isError: true,\n };\n }\n });\n });\n\n return server;\n}\n\n// ---------------------------------------------------------------------------\n// Auth — reuses the same pattern as A2A (Bearer token or JWT). Shared so the\n// HTTP mount and any stdio-side auth-aware helper resolve identity identically.\n// ---------------------------------------------------------------------------\n\nexport function getAccessTokens(): string[] {\n const single = process.env.ACCESS_TOKEN;\n const multi = process.env.ACCESS_TOKENS;\n const tokens: string[] = [];\n if (single) tokens.push(single);\n if (multi) {\n tokens.push(\n ...multi\n .split(\",\")\n .map((t) => t.trim())\n .filter(Boolean),\n );\n }\n return tokens;\n}\n\n/**\n * Resolve the caller identity for a static-token (or dev-open) auth path.\n *\n * Static `ACCESS_TOKEN` / `ACCESS_TOKENS` auth carries no per-caller claims,\n * so without this the MCP endpoint would run every tool with\n * `userEmail === undefined` and per-user / per-org scoped actions\n * (`accessFilter`, `resolveAccess`, `resolveCredential`) would return\n * empty / wrong data. The `agent-native mcp install` flow writes\n * `AGENT_NATIVE_OWNER_EMAIL` into the client config env and the stdio proxy\n * forwards it as the `X-Agent-Native-Owner-Email` request header (see\n * `mcp/stdio.ts#authHeaders`). We trust that owner hint *only* on the\n * static-token path — JWT auth already carries a cryptographically verified\n * `sub`, so the header is ignored there and never widens JWT scope.\n *\n * Precedence is server-trusted-first: the server process's\n * `AGENT_NATIVE_OWNER_EMAIL` env (set out-of-band by the operator / deploy)\n * ALWAYS wins, and a client-supplied `X-Agent-Native-Owner-Email` header is\n * honored *only as a fallback when that env is unset*. A static `ACCESS_TOKEN`\n * is a shared bearer secret; letting a request header override a\n * server-configured owner would let anyone holding a leaked token act as any\n * user. The header path remains for the single-tenant local-dev install flow\n * where the app server process has no owner env and the token *is* the\n * workspace secret; multi-tenant deployments must use A2A JWT (verified `sub`),\n * not a static token, for per-user scope.\n *\n * Returns `undefined` when no owner email is available (true dev-open: no\n * token, no secret, no owner) so behavior there stays unchanged.\n */\nfunction deriveStaticTokenIdentity(\n ownerEmailHeader: string | undefined,\n): MCPCallerIdentity | undefined {\n const owner =\n process.env.AGENT_NATIVE_OWNER_EMAIL?.trim() ||\n (typeof ownerEmailHeader === \"string\" && ownerEmailHeader.trim()) ||\n \"\";\n if (!owner) return undefined;\n return { userEmail: owner, orgDomain: undefined };\n}\n\n/**\n * Verify the inbound auth header. Returns:\n * - { authed: true, identity } when verified — `identity` is derived from\n * the JWT (`sub` / `org_domain`) for JWT auth, or from the\n * `AGENT_NATIVE_OWNER_EMAIL` env / `X-Agent-Native-Owner-Email` header\n * for static-token auth (the `agent-native mcp install` flow). `identity`\n * is undefined only for true dev-open with no owner hint.\n * - { authed: false } on rejection.\n *\n * When A2A_SECRET is set we extract the JWT's `sub` (caller email) and\n * `org_domain` claims so the MCP endpoint can wrap tool runs in\n * `runWithRequestContext({ userEmail, orgId })`. Without that wrap, the\n * MCP endpoint loses tenant identity and downstream `accessFilter` /\n * `resolveCredential` calls fall back to platform-wide defaults.\n *\n * `ownerEmailHeader` is the forwarded `X-Agent-Native-Owner-Email` value; it\n * is consulted ONLY on the static-token / dev-open path (never to influence\n * verified JWT identity), so the install flow runs tools as the configured\n * owner instead of an unscoped anonymous caller.\n */\nexport async function verifyAuth(\n authHeader: string | undefined,\n ownerEmailHeader?: string | undefined,\n): Promise<{ authed: boolean; identity?: MCPCallerIdentity }> {\n // No auth configured → allow (dev mode). Still honour an owner hint\n // (env or forwarded header) so the install flow stays tenant-scoped.\n const accessTokens = getAccessTokens();\n const hasA2ASecret = !!process.env.A2A_SECRET;\n if (accessTokens.length === 0 && !hasA2ASecret) {\n return {\n authed: true,\n identity: deriveStaticTokenIdentity(ownerEmailHeader),\n };\n }\n\n if (!authHeader?.startsWith(\"Bearer \")) return { authed: false };\n const token = authHeader.slice(7);\n\n // Try JWT via A2A_SECRET\n if (hasA2ASecret) {\n try {\n const jose = await import(\"jose\");\n const { payload } = await jose.jwtVerify(\n token,\n new TextEncoder().encode(process.env.A2A_SECRET!),\n );\n return {\n authed: true,\n identity: {\n userEmail: typeof payload.sub === \"string\" ? payload.sub : undefined,\n orgDomain:\n typeof payload.org_domain === \"string\"\n ? (payload.org_domain as string)\n : undefined,\n },\n };\n } catch {\n // Not a valid JWT — fall through to token check\n }\n }\n\n // Try ACCESS_TOKEN / ACCESS_TOKENS exact match. Static tokens carry no\n // per-caller claims, so derive identity from the forwarded owner-email\n // hint (install flow) — otherwise tools would run unscoped.\n if (accessTokens.length > 0 && accessTokens.includes(token)) {\n return {\n authed: true,\n identity: deriveStaticTokenIdentity(ownerEmailHeader),\n };\n }\n\n return { authed: false };\n}\n\nexport async function resolveOrgIdFromDomain(\n orgDomain: string | undefined,\n): Promise<string | undefined> {\n if (!orgDomain) return undefined;\n try {\n const { resolveOrgByDomain } = await import(\"../org/context.js\");\n const org = await resolveOrgByDomain(orgDomain);\n return org?.orgId ?? undefined;\n } catch {\n return undefined;\n }\n}\n"]}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Generic cross-app MCP tools — a stable verb set every external agent gets
3
+ * regardless of which template it is talking to.
4
+ *
5
+ * These are merged into the MCP action registry by
6
+ * `createMCPServerForRequest` (see `build-server.ts`). **Precedence: template
7
+ * actions win.** If a template defines an action named `list_apps` /
8
+ * `open_app` / `ask_app` / `create_workspace_app` / `list_templates`, the
9
+ * template's `ActionEntry` overwrites the builtin of the same name. This is
10
+ * the same template-over-framework precedence `autoDiscoverActions` uses.
11
+ *
12
+ * | Tool | Side effects | Returns |
13
+ * | --------------------- | ------------ | ---------------------------------------- |
14
+ * | `list_apps` | none | `{ apps: [{ id, url, running }] }` |
15
+ * | `open_app` | none | `{ url }` (+ deep-link `link`) |
16
+ * | `ask_app` | agent loop | `{ app, routedVia, response }` |
17
+ * | `create_workspace_app`| scaffolds | `{ name, url, port, deepLink }` (+ link) |
18
+ *
19
+ * `open_app` / `create_workspace_app` return an **absolute** URL on the
20
+ * *target* app's origin when it differs from this app (so a workspace link
21
+ * lands in the right app), and a relative path for the same app / standalone.
22
+ * `ask_app` routes to a *different* workspace app over A2A when possible and
23
+ * reports `routedVia: "a2a"`; otherwise it answers locally
24
+ * (`routedVia: "local"`) and never falsely claims cross-app delegation.
25
+ * | `list_templates` | none | `{ templates: [...] }` (allow-list only) |
26
+ *
27
+ * Node-only at call time (workspace resolution + scaffolding use `fs`), but
28
+ * the module has no top-level Node imports so it bundles fine alongside
29
+ * `mountMCP` — the Node bits are dynamically imported inside `run()`.
30
+ */
31
+ import type { ActionEntry } from "../agent/production-agent.js";
32
+ import type { MCPConfig } from "./build-server.js";
33
+ /**
34
+ * Build the generic cross-app builtin tool registry. Called by
35
+ * `createMCPServerForRequest`; the result is merged UNDER the config's
36
+ * actions so template actions of the same name win.
37
+ */
38
+ export declare function getBuiltinCrossAppTools(config: MCPConfig): Record<string, ActionEntry>;
39
+ //# sourceMappingURL=builtin-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin-tools.d.ts","sourceRoot":"","sources":["../../src/mcp/builtin-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAEhE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AA+ZnD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,GAChB,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAQ7B"}