@agent-native/core 0.17.1 → 0.18.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 (78) 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 +27 -0
  15. package/dist/db/client.d.ts.map +1 -1
  16. package/dist/db/client.js +62 -19
  17. package/dist/db/client.js.map +1 -1
  18. package/dist/db/create-get-db.d.ts.map +1 -1
  19. package/dist/db/create-get-db.js +6 -9
  20. package/dist/db/create-get-db.js.map +1 -1
  21. package/dist/db/index.d.ts.map +1 -1
  22. package/dist/db/index.js +2 -1
  23. package/dist/db/index.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/mcp/build-server.d.ts +135 -0
  28. package/dist/mcp/build-server.d.ts.map +1 -0
  29. package/dist/mcp/build-server.js +274 -0
  30. package/dist/mcp/build-server.js.map +1 -0
  31. package/dist/mcp/builtin-tools.d.ts +32 -0
  32. package/dist/mcp/builtin-tools.d.ts.map +1 -0
  33. package/dist/mcp/builtin-tools.js +299 -0
  34. package/dist/mcp/builtin-tools.js.map +1 -0
  35. package/dist/mcp/index.d.ts +7 -0
  36. package/dist/mcp/index.d.ts.map +1 -1
  37. package/dist/mcp/index.js +8 -0
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/mcp/server.d.ts +3 -13
  40. package/dist/mcp/server.d.ts.map +1 -1
  41. package/dist/mcp/server.js +21 -175
  42. package/dist/mcp/server.js.map +1 -1
  43. package/dist/mcp/stdio.d.ts +44 -0
  44. package/dist/mcp/stdio.d.ts.map +1 -0
  45. package/dist/mcp/stdio.js +208 -0
  46. package/dist/mcp/stdio.js.map +1 -0
  47. package/dist/mcp/workspace-resolve.d.ts +68 -0
  48. package/dist/mcp/workspace-resolve.d.ts.map +1 -0
  49. package/dist/mcp/workspace-resolve.js +205 -0
  50. package/dist/mcp/workspace-resolve.js.map +1 -0
  51. package/dist/server/action-discovery.d.ts.map +1 -1
  52. package/dist/server/action-discovery.js +3 -0
  53. package/dist/server/action-discovery.js.map +1 -1
  54. package/dist/server/auth.d.ts +9 -0
  55. package/dist/server/auth.d.ts.map +1 -1
  56. package/dist/server/auth.js +25 -0
  57. package/dist/server/auth.js.map +1 -1
  58. package/dist/server/better-auth-instance.d.ts.map +1 -1
  59. package/dist/server/better-auth-instance.js +15 -10
  60. package/dist/server/better-auth-instance.js.map +1 -1
  61. package/dist/server/core-routes-plugin.d.ts +5 -0
  62. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  63. package/dist/server/core-routes-plugin.js +9 -0
  64. package/dist/server/core-routes-plugin.js.map +1 -1
  65. package/dist/server/deep-link.d.ts +55 -0
  66. package/dist/server/deep-link.d.ts.map +1 -0
  67. package/dist/server/deep-link.js +69 -0
  68. package/dist/server/deep-link.js.map +1 -0
  69. package/dist/server/index.d.ts +2 -0
  70. package/dist/server/index.d.ts.map +1 -1
  71. package/dist/server/index.js +2 -0
  72. package/dist/server/index.js.map +1 -1
  73. package/dist/server/open-route.d.ts +12 -0
  74. package/dist/server/open-route.d.ts.map +1 -0
  75. package/dist/server/open-route.js +128 -0
  76. package/dist/server/open-route.js.map +1 -0
  77. package/docs/content/external-agents.md +177 -0
  78. package/package.json +1 -1
@@ -0,0 +1,135 @@
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
+ /** App description */
25
+ description: string;
26
+ /** Version string (default "1.0.0") */
27
+ version?: string;
28
+ /** Action registry — same as agent chat and A2A */
29
+ actions: Record<string, ActionEntry>;
30
+ /** Handler for the ask-agent meta-tool — runs the full agent loop */
31
+ askAgent?: (message: string) => Promise<string>;
32
+ /**
33
+ * Disable the generic cross-app builtin tools (`list_apps`, `open_app`,
34
+ * `ask_app`, `create_workspace_app`, `list_templates`). They are merged in
35
+ * by default so external agents get a stable verb set; a template action of
36
+ * the same name always wins (template precedence). Set to `false` only for
37
+ * a constrained / locked-down mount.
38
+ */
39
+ builtinCrossAppTools?: boolean;
40
+ }
41
+ /**
42
+ * Identity extracted from a verified MCP bearer token / JWT. Used to wrap
43
+ * `entry.run()` and `config.askAgent()` calls in `runWithRequestContext`
44
+ * so downstream tools (db-query, accessFilter, resolveCredential) honour
45
+ * per-user / per-org scoping. Without this wrap the MCP endpoint would
46
+ * silently bypass tenant isolation. See finding #6 in
47
+ * /tmp/security-audit/12-mcp-a2a-agent.md.
48
+ */
49
+ export interface MCPCallerIdentity {
50
+ userEmail: string | undefined;
51
+ orgDomain: string | undefined;
52
+ }
53
+ /** Per-request context used to turn an action's relative deep link into the
54
+ * absolute web URL (and desktop `agentnative://` URL) the external agent
55
+ * surfaces. Derived from the inbound request headers in `mountMCP`, or from
56
+ * the resolved local app origin in the stdio standalone path. */
57
+ export interface MCPRequestMeta {
58
+ /** Origin of the running app, e.g. `http://localhost:8100`. */
59
+ origin?: string;
60
+ /** Optional client preference for which URL the *markdown* link uses. */
61
+ target?: "browser" | "desktop" | "terminal";
62
+ }
63
+ /**
64
+ * Build the deep-link content block + structured `_meta` for a tool result.
65
+ * Best-effort: any throw / nullish link is swallowed so a bad `link` builder
66
+ * never fails the tool call.
67
+ */
68
+ export declare function buildLinkArtifacts(entry: ActionEntry, args: Record<string, any>, result: any, meta: MCPRequestMeta | undefined): {
69
+ block?: {
70
+ type: "text";
71
+ text: string;
72
+ };
73
+ _meta?: Record<string, unknown>;
74
+ };
75
+ /**
76
+ * Build a fully-wired MCP `Server` for a single request / session.
77
+ *
78
+ * Shared by the stateless Streamable-HTTP mount (`mountMCP`) and the stdio
79
+ * standalone transport. The HTTP mount passes the per-request origin via
80
+ * `requestMeta`; the stdio standalone path passes the resolved local app
81
+ * origin so deep links still become absolute URLs.
82
+ */
83
+ export declare function createMCPServerForRequest(config: MCPConfig, identity: MCPCallerIdentity | undefined, requestMeta?: MCPRequestMeta): Promise<import("@modelcontextprotocol/sdk/server").Server<{
84
+ method: string;
85
+ params?: {
86
+ [x: string]: unknown;
87
+ _meta?: {
88
+ [x: string]: unknown;
89
+ progressToken?: string | number;
90
+ "io.modelcontextprotocol/related-task"?: {
91
+ taskId: string;
92
+ };
93
+ };
94
+ };
95
+ }, {
96
+ method: string;
97
+ params?: {
98
+ [x: string]: unknown;
99
+ _meta?: {
100
+ [x: string]: unknown;
101
+ progressToken?: string | number;
102
+ "io.modelcontextprotocol/related-task"?: {
103
+ taskId: string;
104
+ };
105
+ };
106
+ };
107
+ }, {
108
+ [x: string]: unknown;
109
+ _meta?: {
110
+ [x: string]: unknown;
111
+ progressToken?: string | number;
112
+ "io.modelcontextprotocol/related-task"?: {
113
+ taskId: string;
114
+ };
115
+ };
116
+ }>>;
117
+ export declare function getAccessTokens(): string[];
118
+ /**
119
+ * Verify the inbound auth header. Returns:
120
+ * - { authed: true, identity } when verified — `identity` may be empty
121
+ * when authed via a static ACCESS_TOKEN (no caller email available).
122
+ * - { authed: false } on rejection.
123
+ *
124
+ * When A2A_SECRET is set we extract the JWT's `sub` (caller email) and
125
+ * `org_domain` claims so the MCP endpoint can wrap tool runs in
126
+ * `runWithRequestContext({ userEmail, orgId })`. Without that wrap, the
127
+ * MCP endpoint loses tenant identity and downstream `accessFilter` /
128
+ * `resolveCredential` calls fall back to platform-wide defaults.
129
+ */
130
+ export declare function verifyAuth(authHeader: string | undefined): Promise<{
131
+ authed: boolean;
132
+ identity?: MCPCallerIdentity;
133
+ }>;
134
+ export declare function resolveOrgIdFromDomain(orgDomain: string | undefined): Promise<string | undefined>;
135
+ //# 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,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAsI7B;AAOD,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAc1C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CAAE,CAAC,CAwC5D;AAED,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS7B"}
@@ -0,0 +1,274 @@
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 orgId once per request (DB lookup) so subsequent wraps are
94
+ // synchronous. The caller identity may be undefined for ACCESS_TOKEN
95
+ // auth — in that case we run with no userEmail/orgId, which makes
96
+ // downstream tools that require per-user scope return empty results
97
+ // rather than cross-tenant data (the safe default).
98
+ const orgIdPromise = resolveOrgIdFromDomain(identity?.orgDomain);
99
+ /**
100
+ * Wrap a callback in `runWithRequestContext({ userEmail, orgId }, fn)`.
101
+ * Both the tools/list and tools/call handlers go through this so
102
+ * downstream `accessFilter`, `resolveCredential`, and per-user MCP
103
+ * visibility checks see the verified caller's identity.
104
+ */
105
+ async function withCallerContext(fn) {
106
+ const orgId = await orgIdPromise;
107
+ return runWithRequestContext({ userEmail: identity?.userEmail, orgId }, fn);
108
+ }
109
+ // tools/list — return all actions + ask-agent meta-tool. Wrapped in the
110
+ // request context so per-user MCP visibility (mcp-client/visibility.ts)
111
+ // applies to the listing too.
112
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
113
+ return withCallerContext(async () => {
114
+ const tools = Object.entries(actions).map(([name, entry]) => {
115
+ const hasLink = typeof entry.link === "function";
116
+ const baseDescription = entry.tool.description ?? name;
117
+ return {
118
+ name,
119
+ description: hasLink
120
+ ? `${baseDescription} After calling, surface the returned "Open in … →" link to the user.`
121
+ : baseDescription,
122
+ inputSchema: entry.tool.parameters ?? {
123
+ type: "object",
124
+ properties: {},
125
+ },
126
+ ...(hasLink
127
+ ? { annotations: { "agent-native/producesOpenLink": true } }
128
+ : {}),
129
+ };
130
+ });
131
+ if (config.askAgent) {
132
+ tools.push({
133
+ name: "ask-agent",
134
+ description: "Send a natural-language message to the app's AI agent and get a response. " +
135
+ "Use this for complex, multi-step tasks that require the agent's reasoning " +
136
+ "and full context about the app.",
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ message: {
141
+ type: "string",
142
+ description: "The message to send to the agent",
143
+ },
144
+ },
145
+ required: ["message"],
146
+ },
147
+ });
148
+ }
149
+ return { tools };
150
+ });
151
+ });
152
+ // tools/call — dispatch to action registry or ask-agent. Wrapped in the
153
+ // request context so the action's `run(args)` and `askAgent()` execute
154
+ // with the verified caller's identity, not the platform default.
155
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
156
+ return withCallerContext(async () => {
157
+ const { name, arguments: args } = request.params;
158
+ if (name === "ask-agent" && config.askAgent) {
159
+ const message = args?.message ?? "";
160
+ try {
161
+ const result = await config.askAgent(message);
162
+ return { content: [{ type: "text", text: result }] };
163
+ }
164
+ catch (err) {
165
+ return {
166
+ content: [{ type: "text", text: `Error: ${err.message}` }],
167
+ isError: true,
168
+ };
169
+ }
170
+ }
171
+ const entry = actions[name];
172
+ if (!entry) {
173
+ return {
174
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
175
+ isError: true,
176
+ };
177
+ }
178
+ try {
179
+ const result = await entry.run(args ?? {});
180
+ const text = typeof result === "string" ? result : JSON.stringify(result);
181
+ const content = [{ type: "text", text }];
182
+ const { block, _meta } = buildLinkArtifacts(entry, args ?? {}, result, requestMeta);
183
+ if (block)
184
+ content.push(block);
185
+ return { content, ...(_meta ? { _meta } : {}) };
186
+ }
187
+ catch (err) {
188
+ return {
189
+ content: [{ type: "text", text: `Error: ${err.message}` }],
190
+ isError: true,
191
+ };
192
+ }
193
+ });
194
+ });
195
+ return server;
196
+ }
197
+ // ---------------------------------------------------------------------------
198
+ // Auth — reuses the same pattern as A2A (Bearer token or JWT). Shared so the
199
+ // HTTP mount and any stdio-side auth-aware helper resolve identity identically.
200
+ // ---------------------------------------------------------------------------
201
+ export function getAccessTokens() {
202
+ const single = process.env.ACCESS_TOKEN;
203
+ const multi = process.env.ACCESS_TOKENS;
204
+ const tokens = [];
205
+ if (single)
206
+ tokens.push(single);
207
+ if (multi) {
208
+ tokens.push(...multi
209
+ .split(",")
210
+ .map((t) => t.trim())
211
+ .filter(Boolean));
212
+ }
213
+ return tokens;
214
+ }
215
+ /**
216
+ * Verify the inbound auth header. Returns:
217
+ * - { authed: true, identity } when verified — `identity` may be empty
218
+ * when authed via a static ACCESS_TOKEN (no caller email available).
219
+ * - { authed: false } on rejection.
220
+ *
221
+ * When A2A_SECRET is set we extract the JWT's `sub` (caller email) and
222
+ * `org_domain` claims so the MCP endpoint can wrap tool runs in
223
+ * `runWithRequestContext({ userEmail, orgId })`. Without that wrap, the
224
+ * MCP endpoint loses tenant identity and downstream `accessFilter` /
225
+ * `resolveCredential` calls fall back to platform-wide defaults.
226
+ */
227
+ export async function verifyAuth(authHeader) {
228
+ // No auth configured → allow (dev mode), but no identity to propagate.
229
+ const accessTokens = getAccessTokens();
230
+ const hasA2ASecret = !!process.env.A2A_SECRET;
231
+ if (accessTokens.length === 0 && !hasA2ASecret) {
232
+ return { authed: true };
233
+ }
234
+ if (!authHeader?.startsWith("Bearer "))
235
+ return { authed: false };
236
+ const token = authHeader.slice(7);
237
+ // Try JWT via A2A_SECRET
238
+ if (hasA2ASecret) {
239
+ try {
240
+ const jose = await import("jose");
241
+ const { payload } = await jose.jwtVerify(token, new TextEncoder().encode(process.env.A2A_SECRET));
242
+ return {
243
+ authed: true,
244
+ identity: {
245
+ userEmail: typeof payload.sub === "string" ? payload.sub : undefined,
246
+ orgDomain: typeof payload.org_domain === "string"
247
+ ? payload.org_domain
248
+ : undefined,
249
+ },
250
+ };
251
+ }
252
+ catch {
253
+ // Not a valid JWT — fall through to token check
254
+ }
255
+ }
256
+ // Try ACCESS_TOKEN / ACCESS_TOKENS exact match (no per-caller identity).
257
+ if (accessTokens.length > 0 && accessTokens.includes(token)) {
258
+ return { authed: true };
259
+ }
260
+ return { authed: false };
261
+ }
262
+ export async function resolveOrgIdFromDomain(orgDomain) {
263
+ if (!orgDomain)
264
+ return undefined;
265
+ try {
266
+ const { resolveOrgByDomain } = await import("../org/context.js");
267
+ const org = await resolveOrgByDomain(orgDomain);
268
+ return org?.orgId ?? undefined;
269
+ }
270
+ catch {
271
+ return undefined;
272
+ }
273
+ }
274
+ //# 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 /** 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 orgId once per request (DB lookup) so subsequent wraps are\n // synchronous. The caller identity may be undefined for ACCESS_TOKEN\n // auth — in that case we run with no userEmail/orgId, which makes\n // downstream tools that require per-user scope return empty results\n // rather than cross-tenant data (the safe default).\n const orgIdPromise = resolveOrgIdFromDomain(identity?.orgDomain);\n\n /**\n * Wrap a callback in `runWithRequestContext({ userEmail, orgId }, 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.\n */\n async function withCallerContext<T>(fn: () => Promise<T>): Promise<T> {\n const orgId = await orgIdPromise;\n return runWithRequestContext(\n { userEmail: identity?.userEmail, orgId },\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 * Verify the inbound auth header. Returns:\n * - { authed: true, identity } when verified — `identity` may be empty\n * when authed via a static ACCESS_TOKEN (no caller email available).\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 */\nexport async function verifyAuth(\n authHeader: string | undefined,\n): Promise<{ authed: boolean; identity?: MCPCallerIdentity }> {\n // No auth configured → allow (dev mode), but no identity to propagate.\n const accessTokens = getAccessTokens();\n const hasA2ASecret = !!process.env.A2A_SECRET;\n if (accessTokens.length === 0 && !hasA2ASecret) {\n return { authed: true };\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 (no per-caller identity).\n if (accessTokens.length > 0 && accessTokens.includes(token)) {\n return { authed: true };\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,32 @@
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, response }` |
17
+ * | `create_workspace_app`| scaffolds | `{ name, url, port, deepLink }` (+ link) |
18
+ * | `list_templates` | none | `{ templates: [...] }` (allow-list only) |
19
+ *
20
+ * Node-only at call time (workspace resolution + scaffolding use `fs`), but
21
+ * the module has no top-level Node imports so it bundles fine alongside
22
+ * `mountMCP` — the Node bits are dynamically imported inside `run()`.
23
+ */
24
+ import type { ActionEntry } from "../agent/production-agent.js";
25
+ import type { MCPConfig } from "./build-server.js";
26
+ /**
27
+ * Build the generic cross-app builtin tool registry. Called by
28
+ * `createMCPServerForRequest`; the result is merged UNDER the config's
29
+ * actions so template actions of the same name win.
30
+ */
31
+ export declare function getBuiltinCrossAppTools(config: MCPConfig): Record<string, ActionEntry>;
32
+ //# 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;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAEhE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAuTnD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,GAChB,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAQ7B"}