@intentface/latch-mcp 0.9.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.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/approval.test.d.ts +2 -0
- package/dist/approval.test.d.ts.map +1 -0
- package/dist/approval.test.js +64 -0
- package/dist/approval.test.js.map +1 -0
- package/dist/connections.d.ts +180 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +255 -0
- package/dist/connections.js.map +1 -0
- package/dist/host-output.test.d.ts +2 -0
- package/dist/host-output.test.d.ts.map +1 -0
- package/dist/host-output.test.js +51 -0
- package/dist/host-output.test.js.map +1 -0
- package/dist/host.d.ts +203 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +259 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth-fetch.d.ts +45 -0
- package/dist/oauth-fetch.d.ts.map +1 -0
- package/dist/oauth-fetch.js +106 -0
- package/dist/oauth-fetch.js.map +1 -0
- package/dist/oauth.d.ts +120 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +202 -0
- package/dist/oauth.js.map +1 -0
- package/dist/oauth.test.d.ts +2 -0
- package/dist/oauth.test.d.ts.map +1 -0
- package/dist/oauth.test.js +211 -0
- package/dist/oauth.test.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Intentface
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @intentface/latch-mcp
|
|
2
|
+
|
|
3
|
+
The MCP (Model Context Protocol) host for Latch — connect to MCP servers and expose their tools to agents, with per-tenant isolation and brokered auth.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- **`createMcpHost`** — opens MCP server connections and surfaces their tools as agent tools. SSRF allowlist (`allowedHosts`) bounds which hosts a connection may reach; the server token/auth is brokered server-side and never reaches the model.
|
|
8
|
+
- **`createMcpConnections`** — a `ConnectionRegistry` over a set of declared MCP connections (static `bearer`/`header` token, or interactive `oauth`). OAuth connections expose a gated `connect_<name>` tool so an agent can ask the user to authorize mid-chat (and auto-resume).
|
|
9
|
+
- **`createMcpOAuth`** — the per-connection OAuth flow (authorize/callback/refresh), Vault-backed per caller. Supports **DCR** (dynamic client registration) and **pre-registered** clients (for providers without DCR — Google/GitHub/Slack).
|
|
10
|
+
- **`createOAuthDiscoveryFetch`** — normalizes non-conformant discovery docs from proxied servers (force `https`, strip trailing-slash issuer) so the SDK's strict RFC checks pass.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createMcpConnections } from "@intentface/latch-mcp";
|
|
16
|
+
|
|
17
|
+
const connections = createMcpConnections<Principal>({
|
|
18
|
+
callbackBaseUrl: `${baseURL}/api/latch/connections`,
|
|
19
|
+
connections: connectionDefs, // { name: { url, auth, normalize? } }
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Where it fits
|
|
24
|
+
|
|
25
|
+
A connection source for `@intentface/latch-core`'s registry. The platform also exposes user-added (UI/Vault-stored) MCP connections via the same primitives.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval.test.d.ts","sourceRoot":"","sources":["../src/approval.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mcpApprovalValue, needsMcpApproval, nameHeuristicApproval, } from "./index.js";
|
|
3
|
+
/**
|
|
4
|
+
* The approval policy's two-level shape: a policy decides per TOOL (from its
|
|
5
|
+
* descriptor/annotations), and may hand back a per-CALL function that decides
|
|
6
|
+
* from the model's arguments. The call form exists for meta-tool servers —
|
|
7
|
+
* one `execute_tool` multiplexing every operation carries a single annotation
|
|
8
|
+
* set, so a per-tool boolean can only gate everything or nothing.
|
|
9
|
+
*/
|
|
10
|
+
const READ_TOOL = {
|
|
11
|
+
name: "find_many_companies",
|
|
12
|
+
annotations: { readOnlyHint: true },
|
|
13
|
+
};
|
|
14
|
+
const WRITE_TOOL = {
|
|
15
|
+
name: "create_company",
|
|
16
|
+
annotations: { destructiveHint: true },
|
|
17
|
+
};
|
|
18
|
+
const META_TOOL = { name: "execute_tool" };
|
|
19
|
+
describe("needsMcpApproval", () => {
|
|
20
|
+
it('"destructive" (default) keys on annotations', () => {
|
|
21
|
+
expect(needsMcpApproval(undefined, READ_TOOL)).toBe(false);
|
|
22
|
+
expect(needsMcpApproval(undefined, WRITE_TOOL)).toBe(true);
|
|
23
|
+
expect(needsMcpApproval("destructive", META_TOOL)).toBe(false); // no hints
|
|
24
|
+
});
|
|
25
|
+
it("passes a per-call function through from the predicate", () => {
|
|
26
|
+
const perCall = (input) => String(input?.toolName ?? "").startsWith("create_");
|
|
27
|
+
const gate = needsMcpApproval((tool) => (tool.name === "execute_tool" ? perCall : false), META_TOOL);
|
|
28
|
+
expect(gate).toBe(perCall);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("mcpApprovalValue — the SDK toolApproval map value", () => {
|
|
32
|
+
it("boolean policies map to 'user-approval' / undefined", () => {
|
|
33
|
+
expect(mcpApprovalValue("all", READ_TOOL)).toBe("user-approval");
|
|
34
|
+
expect(mcpApprovalValue("none", WRITE_TOOL)).toBeUndefined();
|
|
35
|
+
expect(mcpApprovalValue(undefined, WRITE_TOOL)).toBe("user-approval");
|
|
36
|
+
expect(mcpApprovalValue(undefined, READ_TOOL)).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
it("a per-call policy gates by ARGUMENTS at call time", async () => {
|
|
39
|
+
const value = mcpApprovalValue((tool) => tool.name === "execute_tool"
|
|
40
|
+
? (input) => /^(create|update|delete)_/.test(String(input?.toolName ?? ""))
|
|
41
|
+
: false, META_TOOL);
|
|
42
|
+
expect(typeof value).toBe("function");
|
|
43
|
+
const decide = value;
|
|
44
|
+
// The read that finding 1.2 saw gated — through the meta-tool — runs free…
|
|
45
|
+
expect(await decide({ toolName: "find_many_companies" })).toBeUndefined();
|
|
46
|
+
// …while the mutating operations still stop for a human.
|
|
47
|
+
expect(await decide({ toolName: "create_person" })).toBe("user-approval");
|
|
48
|
+
expect(await decide({ toolName: "delete_company" })).toBe("user-approval");
|
|
49
|
+
});
|
|
50
|
+
it("supports an async per-call decision", async () => {
|
|
51
|
+
const value = mcpApprovalValue(() => async () => true, META_TOOL);
|
|
52
|
+
const decide = value;
|
|
53
|
+
expect(await decide({})).toBe("user-approval");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("nameHeuristicApproval (unchanged behavior)", () => {
|
|
57
|
+
it("annotations stay authoritative; verb heuristic only without hints", () => {
|
|
58
|
+
const policy = nameHeuristicApproval();
|
|
59
|
+
expect(policy(READ_TOOL)).toBe(false); // readOnlyHint wins
|
|
60
|
+
expect(policy({ name: "create_deal" })).toBe(true); // verb, no hints
|
|
61
|
+
expect(policy({ name: "list_deals" })).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=approval.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval.test.js","sourceRoot":"","sources":["../src/approval.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { OAuthClientMetadata } from "@ai-sdk/mcp";
|
|
2
|
+
import type { SecretResolver, Vault, ToolSource } from "@intentface/latch-core";
|
|
3
|
+
import type { McpApprovalPolicy, McpToolInfo } from "./host.js";
|
|
4
|
+
import type { McpOAuth } from "./oauth.js";
|
|
5
|
+
/**
|
|
6
|
+
* Declarative MCP connections — the developer-facing surface.
|
|
7
|
+
*
|
|
8
|
+
* A customer writes one `defineMcpConnection({ url, description, auth })` per
|
|
9
|
+
* server (eve-style), then hands the set to `createMcpConnections`. The registry
|
|
10
|
+
* owns everything else: tool discovery, the SSRF allowlist, OAuth (PKCE/DCR,
|
|
11
|
+
* token storage in the Vault, discovery-doc normalization), and the synthetic
|
|
12
|
+
* `connect_<name>` tool that lets the agent ask the user to authorize mid-chat.
|
|
13
|
+
* The handler (`@intentface/latch-server`) drives the OAuth routes off it.
|
|
14
|
+
*/
|
|
15
|
+
/** How a connection authenticates. Build with `none` / `bearer` / `headers` / `oauth`. */
|
|
16
|
+
export type McpConnectionAuth<P> = {
|
|
17
|
+
kind: "none";
|
|
18
|
+
} | {
|
|
19
|
+
kind: "bearer";
|
|
20
|
+
getToken: (principal: P) => string | Promise<string>;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "headers";
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "oauth";
|
|
26
|
+
options: OAuthAuthOptions;
|
|
27
|
+
};
|
|
28
|
+
/** Options for an interactive (Authorization Code + PKCE) OAuth connection. */
|
|
29
|
+
export interface OAuthAuthOptions {
|
|
30
|
+
/** Requested scope string components, joined with spaces. */
|
|
31
|
+
scopes?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* A pre-registered OAuth client. When set, Dynamic Client Registration (DCR)
|
|
34
|
+
* is SKIPPED and this client is used directly — required for auth servers that
|
|
35
|
+
* don't support DCR (Google, GitHub, Slack, …). Register our callback URL
|
|
36
|
+
* (`<callbackBaseUrl>/<name>/callback`) as an allowed redirect URI with the
|
|
37
|
+
* provider. Omit `clientSecret` for a public PKCE client.
|
|
38
|
+
*/
|
|
39
|
+
clientId?: string;
|
|
40
|
+
clientSecret?: string;
|
|
41
|
+
/** Client name advertised during dynamic client registration. */
|
|
42
|
+
clientName?: string;
|
|
43
|
+
/** Token-endpoint auth method (default `"none"` — public PKCE client). */
|
|
44
|
+
tokenEndpointAuthMethod?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Normalize a provider's non-conformant discovery documents (e.g. a proxied
|
|
47
|
+
* `http://` authorization-server reference, or a trailing-slash issuer the
|
|
48
|
+
* SDK's strict RFC 8414 check rejects). `forceScheme: "https"` is the common
|
|
49
|
+
* case; `hosts` bounds the rewrite to those hostnames.
|
|
50
|
+
*/
|
|
51
|
+
normalize?: {
|
|
52
|
+
forceScheme?: "https" | "http";
|
|
53
|
+
hosts?: string[];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Omit the RFC 8707 `resource` parameter from the authorization request and
|
|
57
|
+
* token exchange even when the server's protected-resource metadata
|
|
58
|
+
* advertises one. Google's MCP servers (calendarmcp.googleapis.com, …) do
|
|
59
|
+
* advertise a resource, but the audience-bound token Google then mints is
|
|
60
|
+
* rejected by the MCP server itself (401 `invalid_token`); a plain
|
|
61
|
+
* scope-bearing token is accepted. Set `true` for those servers.
|
|
62
|
+
*/
|
|
63
|
+
omitResource?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Extra query params appended to the authorization redirect URL — provider
|
|
66
|
+
* extensions the SDK doesn't model, e.g. Google's
|
|
67
|
+
* `{ access_type: "offline", prompt: "consent" }` to be issued a refresh
|
|
68
|
+
* token (without it the connection silently dies when the 1h access token
|
|
69
|
+
* expires, since Google returns no refresh token by default).
|
|
70
|
+
*/
|
|
71
|
+
authorizationParams?: Record<string, string>;
|
|
72
|
+
/** Extra client-metadata passthrough (merged into the generated metadata). */
|
|
73
|
+
clientMetadata?: Partial<OAuthClientMetadata>;
|
|
74
|
+
}
|
|
75
|
+
export declare const none: {
|
|
76
|
+
readonly kind: "none";
|
|
77
|
+
};
|
|
78
|
+
export declare function bearer<P>(opts: {
|
|
79
|
+
getToken: (principal: P) => string | Promise<string>;
|
|
80
|
+
}): McpConnectionAuth<P>;
|
|
81
|
+
export declare function headers<P = unknown>(headers: Record<string, string>): McpConnectionAuth<P>;
|
|
82
|
+
export declare function oauth<P = unknown>(options?: OAuthAuthOptions): McpConnectionAuth<P>;
|
|
83
|
+
/** A declarative MCP connection. The connection's *name* is the registry key. */
|
|
84
|
+
export interface McpConnectionDef<P = unknown> {
|
|
85
|
+
/** MCP server URL (HTTP transport). */
|
|
86
|
+
url: string;
|
|
87
|
+
/** Human-readable purpose — surfaced to the user; helps the model choose it. */
|
|
88
|
+
description?: string;
|
|
89
|
+
/** Auth strategy (default: none). */
|
|
90
|
+
auth?: McpConnectionAuth<P>;
|
|
91
|
+
/** Restrict the exposed tools by their *server-local* name (pre-namespacing). */
|
|
92
|
+
tools?: {
|
|
93
|
+
allow?: string[];
|
|
94
|
+
block?: string[];
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Which of this connection's tools require human approval. Default
|
|
98
|
+
* `"destructive"` — respect the MCP server's own annotations (gate tools it
|
|
99
|
+
* marks destructive / not read-only). `"all"`, `"none"`, or a per-tool
|
|
100
|
+
* predicate `(tool) => boolean` — which may instead return a per-CALL
|
|
101
|
+
* function `(input) => boolean | Promise<boolean>`, evaluated with the
|
|
102
|
+
* model's arguments each call, for meta-tool servers whose one tool
|
|
103
|
+
* multiplexes every operation (gate `execute_tool` only when
|
|
104
|
+
* `input.toolName` starts with `create_`).
|
|
105
|
+
* For policy that varies by caller, use {@link approvalFor} instead.
|
|
106
|
+
*/
|
|
107
|
+
approval?: McpApprovalPolicy;
|
|
108
|
+
/**
|
|
109
|
+
* Per-principal approval resolver — awaited at open time to produce the
|
|
110
|
+
* approval policy for this caller (e.g. per-user permissions from a DB).
|
|
111
|
+
* Takes precedence over {@link approval} when set.
|
|
112
|
+
*/
|
|
113
|
+
approvalFor?: (principal: P) => McpApprovalPolicy | Promise<McpApprovalPolicy>;
|
|
114
|
+
/**
|
|
115
|
+
* Tool visibility filter — a tool for which it returns `false` is dropped
|
|
116
|
+
* entirely (no tool, no approval entry). A per-tool predicate
|
|
117
|
+
* `(tool) => boolean`, for a hard block an approval policy can't express. For
|
|
118
|
+
* visibility that varies by caller, use {@link visibleToolsFor} instead.
|
|
119
|
+
*/
|
|
120
|
+
visibleTools?: (tool: McpToolInfo) => boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Per-principal visibility resolver — awaited at open time to produce the
|
|
123
|
+
* visibility predicate for this caller. Takes precedence over
|
|
124
|
+
* {@link visibleTools} when set.
|
|
125
|
+
*/
|
|
126
|
+
visibleToolsFor?: (principal: P) => ((tool: McpToolInfo) => boolean) | Promise<(tool: McpToolInfo) => boolean>;
|
|
127
|
+
/**
|
|
128
|
+
* Permit this connection to reach a private/internal address. Off by default
|
|
129
|
+
* (the host blocks private/loopback/link-local IPs and DNS rebinding); set
|
|
130
|
+
* `true` only for a trusted local/in-cluster MCP server.
|
|
131
|
+
*/
|
|
132
|
+
allowPrivateHost?: boolean;
|
|
133
|
+
}
|
|
134
|
+
/** Identity helper — keeps connection files declarative and typed. */
|
|
135
|
+
export declare function defineMcpConnection<P = unknown>(def: McpConnectionDef<P>): McpConnectionDef<P>;
|
|
136
|
+
export interface McpConnectionsConfig<P> {
|
|
137
|
+
vault: Vault<P>;
|
|
138
|
+
/** Read-side for bearer secrets, if any bearer connection resolves from the Vault. */
|
|
139
|
+
secrets?: SecretResolver<P>;
|
|
140
|
+
/** SSRF allowlist (see `createMcpHost`). */
|
|
141
|
+
allowedHosts: string[] | ((principal: P) => string[] | Promise<string[]>);
|
|
142
|
+
/** Connection set, keyed by name. */
|
|
143
|
+
connections: Record<string, McpConnectionDef<P>>;
|
|
144
|
+
/**
|
|
145
|
+
* Base URL the OAuth callback lands on, e.g.
|
|
146
|
+
* `https://app.example.com/api/latch/connections`. The registry appends
|
|
147
|
+
* `/<name>/callback` to build each connection's exact redirect URI.
|
|
148
|
+
*/
|
|
149
|
+
callbackBaseUrl?: string;
|
|
150
|
+
/** Observe a connection that failed to open (skipped, not fatal). */
|
|
151
|
+
onConnectionError?: (name: string, error: unknown) => void;
|
|
152
|
+
}
|
|
153
|
+
/** Per-connection status for a tenant. */
|
|
154
|
+
export interface ConnectionStatus {
|
|
155
|
+
name: string;
|
|
156
|
+
description?: string;
|
|
157
|
+
/** True if this connection uses interactive OAuth. */
|
|
158
|
+
oauth: boolean;
|
|
159
|
+
/** True if usable now (non-OAuth are always true; OAuth → tokens present). */
|
|
160
|
+
authorized: boolean;
|
|
161
|
+
}
|
|
162
|
+
export interface McpConnections<P> {
|
|
163
|
+
/** A `ToolSource` over ALL connections: authorized tools + connect_* tools. */
|
|
164
|
+
host: ToolSource<P>;
|
|
165
|
+
/**
|
|
166
|
+
* A `ToolSource` scoped to the named connections only — what the runtime uses
|
|
167
|
+
* to resolve an agent's `connections: [...]`. Unknown names are ignored.
|
|
168
|
+
*/
|
|
169
|
+
hostFor(names: string[]): ToolSource<P>;
|
|
170
|
+
/** Declared connection names. */
|
|
171
|
+
names: string[];
|
|
172
|
+
/** The OAuth flow for a connection (undefined if it isn't an OAuth connection). */
|
|
173
|
+
oauth(name: string): McpOAuth<P> | undefined;
|
|
174
|
+
/** Per-connection status for the caller. */
|
|
175
|
+
status(principal: P): Promise<ConnectionStatus[]>;
|
|
176
|
+
/** Open just one connection and return its namespaced tool names (a connectivity probe). */
|
|
177
|
+
testTools(name: string, principal: P): Promise<string[]>;
|
|
178
|
+
}
|
|
179
|
+
export declare function createMcpConnections<P>(config: McpConnectionsConfig<P>): McpConnections<P>;
|
|
180
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../src/connections.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { tool, jsonSchema } from "ai";
|
|
2
|
+
import { createMcpHost } from "./host.js";
|
|
3
|
+
import { createMcpOAuth } from "./oauth.js";
|
|
4
|
+
import { createOAuthDiscoveryFetch } from "./oauth-fetch.js";
|
|
5
|
+
export const none = { kind: "none" };
|
|
6
|
+
export function bearer(opts) {
|
|
7
|
+
return { kind: "bearer", getToken: opts.getToken };
|
|
8
|
+
}
|
|
9
|
+
export function headers(headers) {
|
|
10
|
+
return { kind: "headers", headers };
|
|
11
|
+
}
|
|
12
|
+
export function oauth(options = {}) {
|
|
13
|
+
return { kind: "oauth", options };
|
|
14
|
+
}
|
|
15
|
+
/** Identity helper — keeps connection files declarative and typed. */
|
|
16
|
+
export function defineMcpConnection(def) {
|
|
17
|
+
return def;
|
|
18
|
+
}
|
|
19
|
+
/** Build the synthetic tool that asks the user to authorize a connection. */
|
|
20
|
+
function connectTool(name, def) {
|
|
21
|
+
return tool({
|
|
22
|
+
description: `Connect the "${name}" integration` +
|
|
23
|
+
(def.description ? ` (${def.description})` : "") +
|
|
24
|
+
`. The user must authorize access first — calling this prompts them to ` +
|
|
25
|
+
`connect. Only call it when ${name}'s own tools are not already available.`,
|
|
26
|
+
inputSchema: jsonSchema({
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {},
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
}),
|
|
31
|
+
execute: async () => ({ connected: true, connection: name }),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** True if a server-local tool name passes the connection's allow/block filter. */
|
|
35
|
+
function toolAllowed(localName, filter) {
|
|
36
|
+
if (!filter)
|
|
37
|
+
return true;
|
|
38
|
+
if (filter.block?.includes(localName))
|
|
39
|
+
return false;
|
|
40
|
+
if (filter.allow && !filter.allow.includes(localName))
|
|
41
|
+
return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
export function createMcpConnections(config) {
|
|
45
|
+
const entries = Object.entries(config.connections);
|
|
46
|
+
// One McpOAuth + normalizing fetch per OAuth connection, built up front.
|
|
47
|
+
const oauthByName = new Map();
|
|
48
|
+
const fetchByName = new Map();
|
|
49
|
+
for (const [name, def] of entries) {
|
|
50
|
+
if (def.auth?.kind !== "oauth")
|
|
51
|
+
continue;
|
|
52
|
+
const opts = def.auth.options;
|
|
53
|
+
const redirectUrl = config.callbackBaseUrl
|
|
54
|
+
? `${config.callbackBaseUrl.replace(/\/$/, "")}/${name}/callback`
|
|
55
|
+
: undefined;
|
|
56
|
+
const normFetch = opts.normalize
|
|
57
|
+
? createOAuthDiscoveryFetch(opts.normalize)
|
|
58
|
+
: undefined;
|
|
59
|
+
if (normFetch)
|
|
60
|
+
fetchByName.set(name, normFetch);
|
|
61
|
+
if (!redirectUrl) {
|
|
62
|
+
// Without a callback base we can't run the redirect flow; skip building
|
|
63
|
+
// the OAuth client (status will still report it as an OAuth connection).
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const clientMetadata = {
|
|
67
|
+
client_name: opts.clientName ?? `Latch (${name})`,
|
|
68
|
+
redirect_uris: [redirectUrl],
|
|
69
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
70
|
+
response_types: ["code"],
|
|
71
|
+
token_endpoint_auth_method: opts.tokenEndpointAuthMethod ?? "none",
|
|
72
|
+
...opts.clientMetadata,
|
|
73
|
+
};
|
|
74
|
+
oauthByName.set(name, createMcpOAuth({
|
|
75
|
+
vault: config.vault,
|
|
76
|
+
config: {
|
|
77
|
+
name,
|
|
78
|
+
serverUrl: def.url,
|
|
79
|
+
redirectUrl,
|
|
80
|
+
clientMetadata,
|
|
81
|
+
// Pre-registered client (skips DCR) when provided — for auth servers
|
|
82
|
+
// without dynamic registration.
|
|
83
|
+
clientInformation: opts.clientId
|
|
84
|
+
? { client_id: opts.clientId, client_secret: opts.clientSecret }
|
|
85
|
+
: undefined,
|
|
86
|
+
scope: opts.scopes?.join(" "),
|
|
87
|
+
fetch: normFetch,
|
|
88
|
+
// `undefined` keeps the SDK default (exact origin match on the
|
|
89
|
+
// advertised resource); a defined hook returning undefined omits the
|
|
90
|
+
// resource parameter from the authorize + exchange requests.
|
|
91
|
+
validateResourceURL: opts.omitResource ? () => undefined : undefined,
|
|
92
|
+
authorizationParams: opts.authorizationParams,
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
/** Resolve the real (server-backed) connections for a caller this turn. */
|
|
97
|
+
async function resolveConnections(principal) {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const [name, def] of entries) {
|
|
100
|
+
const auth = def.auth ?? none;
|
|
101
|
+
// `*For` resolvers vary by caller (awaited per turn); they take precedence
|
|
102
|
+
// over the static `approval`/`visibleTools`. A resolver failure skips just
|
|
103
|
+
// this connection (graceful degradation, like a connect-time failure)
|
|
104
|
+
// instead of rejecting the whole host.
|
|
105
|
+
let approval = def.approval;
|
|
106
|
+
let visible = def.visibleTools;
|
|
107
|
+
try {
|
|
108
|
+
if (def.approvalFor)
|
|
109
|
+
approval = await def.approvalFor(principal);
|
|
110
|
+
if (def.visibleToolsFor)
|
|
111
|
+
visible = await def.visibleToolsFor(principal);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
config.onConnectionError?.(name, error);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (auth.kind === "oauth") {
|
|
118
|
+
const o = oauthByName.get(name);
|
|
119
|
+
// Include the live connection only once the caller has authorized.
|
|
120
|
+
if (o && (await o.isAuthorized(principal))) {
|
|
121
|
+
out.push({
|
|
122
|
+
name,
|
|
123
|
+
url: def.url,
|
|
124
|
+
authProvider: o.provider(principal),
|
|
125
|
+
fetch: fetchByName.get(name),
|
|
126
|
+
approval,
|
|
127
|
+
visible,
|
|
128
|
+
allowPrivateHost: def.allowPrivateHost,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (auth.kind === "bearer") {
|
|
133
|
+
const token = await auth.getToken(principal);
|
|
134
|
+
out.push({
|
|
135
|
+
name,
|
|
136
|
+
url: def.url,
|
|
137
|
+
auth: { type: "headers", headers: { Authorization: `Bearer ${token}` } },
|
|
138
|
+
approval,
|
|
139
|
+
visible,
|
|
140
|
+
allowPrivateHost: def.allowPrivateHost,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else if (auth.kind === "headers") {
|
|
144
|
+
out.push({
|
|
145
|
+
name,
|
|
146
|
+
url: def.url,
|
|
147
|
+
auth: { type: "headers", headers: auth.headers },
|
|
148
|
+
approval,
|
|
149
|
+
visible,
|
|
150
|
+
allowPrivateHost: def.allowPrivateHost,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
out.push({
|
|
155
|
+
name,
|
|
156
|
+
url: def.url,
|
|
157
|
+
approval,
|
|
158
|
+
visible,
|
|
159
|
+
allowPrivateHost: def.allowPrivateHost,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
const filterByName = new Map(entries.map(([name, def]) => [name, def.tools]));
|
|
166
|
+
/**
|
|
167
|
+
* A tool source over the connections in `only` (or all, if undefined):
|
|
168
|
+
* each authorized connection's filtered tools, plus a gated `connect_<name>`
|
|
169
|
+
* for every OAuth connection in scope (present even when authorized, so an
|
|
170
|
+
* approved historical call can execute on resume).
|
|
171
|
+
*/
|
|
172
|
+
function makeHost(only) {
|
|
173
|
+
const inScope = (name) => !only || only.has(name);
|
|
174
|
+
const hostInstance = createMcpHost({
|
|
175
|
+
secrets: config.secrets,
|
|
176
|
+
allowedHosts: config.allowedHosts,
|
|
177
|
+
onConnectionError: config.onConnectionError,
|
|
178
|
+
connections: async (principal) => (await resolveConnections(principal)).filter((c) => inScope(c.name)),
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
async open(principal) {
|
|
182
|
+
const opened = await hostInstance.open(principal);
|
|
183
|
+
// Apply per-connection allow/block on the server-local tool name.
|
|
184
|
+
const tools = {};
|
|
185
|
+
for (const [key, t] of Object.entries(opened.tools)) {
|
|
186
|
+
const us = key.indexOf("_");
|
|
187
|
+
const conn = us === -1 ? key : key.slice(0, us);
|
|
188
|
+
const local = us === -1 ? key : key.slice(us + 1);
|
|
189
|
+
if (toolAllowed(local, filterByName.get(conn)))
|
|
190
|
+
tools[key] = t;
|
|
191
|
+
}
|
|
192
|
+
// Start from the host's annotation-derived gating (only for the tools
|
|
193
|
+
// that survived the allow/block filter), then add the connect_* gates.
|
|
194
|
+
const toolApproval = {};
|
|
195
|
+
for (const [key, status] of Object.entries(opened.toolApproval)) {
|
|
196
|
+
if (key in tools)
|
|
197
|
+
toolApproval[key] = status;
|
|
198
|
+
}
|
|
199
|
+
const alwaysActive = [];
|
|
200
|
+
for (const [name, def] of entries) {
|
|
201
|
+
if (def.auth?.kind !== "oauth" || !inScope(name))
|
|
202
|
+
continue;
|
|
203
|
+
const key = `connect_${name}`;
|
|
204
|
+
tools[key] = connectTool(name, def);
|
|
205
|
+
toolApproval[key] = "user-approval";
|
|
206
|
+
alwaysActive.push(key); // keep the auth tool callable under enabledTools
|
|
207
|
+
}
|
|
208
|
+
return { tools, close: opened.close, toolApproval, alwaysActive };
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
host: makeHost(),
|
|
214
|
+
hostFor: (names) => makeHost(new Set(names)),
|
|
215
|
+
names: entries.map(([name]) => name),
|
|
216
|
+
oauth: (name) => oauthByName.get(name),
|
|
217
|
+
async status(principal) {
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const [name, def] of entries) {
|
|
220
|
+
const isOAuth = def.auth?.kind === "oauth";
|
|
221
|
+
const o = oauthByName.get(name);
|
|
222
|
+
out.push({
|
|
223
|
+
name,
|
|
224
|
+
description: def.description,
|
|
225
|
+
oauth: isOAuth,
|
|
226
|
+
authorized: isOAuth ? (o ? await o.isAuthorized(principal) : false) : true,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
},
|
|
231
|
+
async testTools(name, principal) {
|
|
232
|
+
const def = config.connections[name];
|
|
233
|
+
if (!def)
|
|
234
|
+
throw new Error(`Unknown connection: ${name}`);
|
|
235
|
+
// Open a one-connection host so the probe doesn't touch other servers.
|
|
236
|
+
const probe = createMcpHost({
|
|
237
|
+
secrets: config.secrets,
|
|
238
|
+
allowedHosts: config.allowedHosts,
|
|
239
|
+
onConnectionError: config.onConnectionError,
|
|
240
|
+
connections: async (p) => {
|
|
241
|
+
const all = await resolveConnections(p);
|
|
242
|
+
return all.filter((c) => c.name === name);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const opened = await probe.open(principal);
|
|
246
|
+
try {
|
|
247
|
+
return Object.keys(opened.tools);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await opened.close();
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=connections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.js","sourceRoot":"","sources":["../src/connections.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-output.test.d.ts","sourceRoot":"","sources":["../src/host-output.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { withStructuredContentFallback } from "./host.js";
|
|
3
|
+
const call = (output, original) => withStructuredContentFallback(original)({ toolCallId: "c1", input: {}, output });
|
|
4
|
+
describe("withStructuredContentFallback", () => {
|
|
5
|
+
it("returns structuredContent as JSON when content is empty (Airtable-style)", () => {
|
|
6
|
+
const output = {
|
|
7
|
+
content: [],
|
|
8
|
+
structuredContent: { bases: [{ id: "app1", name: "ATS" }] },
|
|
9
|
+
isError: false,
|
|
10
|
+
};
|
|
11
|
+
expect(call(output)).toEqual({
|
|
12
|
+
type: "json",
|
|
13
|
+
value: { bases: [{ id: "app1", name: "ATS" }] },
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
it("falls back when content only holds blank text parts", () => {
|
|
17
|
+
const output = {
|
|
18
|
+
content: [{ type: "text", text: " " }],
|
|
19
|
+
structuredContent: { ok: true },
|
|
20
|
+
};
|
|
21
|
+
expect(call(output)).toEqual({ type: "json", value: { ok: true } });
|
|
22
|
+
});
|
|
23
|
+
it("keeps the SDK mapping when content carries real text", () => {
|
|
24
|
+
const output = {
|
|
25
|
+
content: [{ type: "text", text: "3 bases found" }],
|
|
26
|
+
structuredContent: { bases: [] },
|
|
27
|
+
};
|
|
28
|
+
const original = () => ({ type: "content", value: [{ type: "text", text: "3 bases found" }] });
|
|
29
|
+
expect(call(output, original)).toEqual({
|
|
30
|
+
type: "content",
|
|
31
|
+
value: [{ type: "text", text: "3 bases found" }],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it("keeps the SDK mapping for non-text parts (images)", () => {
|
|
35
|
+
const output = {
|
|
36
|
+
content: [{ type: "image", data: "…", mimeType: "image/png" }],
|
|
37
|
+
structuredContent: { alt: "chart" },
|
|
38
|
+
};
|
|
39
|
+
const original = () => "sdk";
|
|
40
|
+
expect(call(output, original)).toBe("sdk");
|
|
41
|
+
});
|
|
42
|
+
it("leaves errors alone even with structuredContent present", () => {
|
|
43
|
+
const output = { content: [], structuredContent: { reason: "x" }, isError: true };
|
|
44
|
+
const original = () => "sdk-error-path";
|
|
45
|
+
expect(call(output, original)).toBe("sdk-error-path");
|
|
46
|
+
});
|
|
47
|
+
it("passes plain results through as JSON when no original mapper exists", () => {
|
|
48
|
+
expect(call({ anything: 1 })).toEqual({ type: "json", value: { anything: 1 } });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=host-output.test.js.map
|