@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.
- package/dist/action.d.ts +27 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +2 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +4 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts +16 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +583 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/db/client.d.ts +27 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +62 -19
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +6 -9
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +2 -1
- package/dist/db/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/build-server.d.ts +135 -0
- package/dist/mcp/build-server.d.ts.map +1 -0
- package/dist/mcp/build-server.js +274 -0
- package/dist/mcp/build-server.js.map +1 -0
- package/dist/mcp/builtin-tools.d.ts +32 -0
- package/dist/mcp/builtin-tools.d.ts.map +1 -0
- package/dist/mcp/builtin-tools.js +299 -0
- package/dist/mcp/builtin-tools.js.map +1 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +3 -13
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +21 -175
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.d.ts +44 -0
- package/dist/mcp/stdio.d.ts.map +1 -0
- package/dist/mcp/stdio.js +208 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/workspace-resolve.d.ts +68 -0
- package/dist/mcp/workspace-resolve.d.ts.map +1 -0
- package/dist/mcp/workspace-resolve.js +205 -0
- package/dist/mcp/workspace-resolve.js.map +1 -0
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +3 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/auth.d.ts +9 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +25 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +15 -10
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +5 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +9 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/deep-link.d.ts +55 -0
- package/dist/server/deep-link.d.ts.map +1 -0
- package/dist/server/deep-link.js +69 -0
- package/dist/server/deep-link.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/open-route.d.ts +12 -0
- package/dist/server/open-route.d.ts.map +1 -0
- package/dist/server/open-route.js +128 -0
- package/dist/server/open-route.js.map +1 -0
- package/docs/content/external-agents.md +177 -0
- 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"}
|