agent-trellis 0.11.0 → 0.12.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/adapters/mcpPlan.d.ts +1 -0
- package/dist/adapters/mcpPlan.js +14 -5
- package/dist/cli.js +1 -1
- package/dist/commands/mcp.d.ts +13 -2
- package/dist/commands/mcp.js +42 -5
- package/dist/commands/mcpAuth.d.ts +6 -0
- package/dist/commands/mcpAuth.js +38 -10
- package/dist/commands/mcpGateway.js +7 -7
- package/dist/core/canonical.d.ts +1 -0
- package/dist/core/canonical.js +8 -3
- package/dist/core/types.d.ts +4 -1
- package/dist/core/types.js +3 -0
- package/dist/lib/agentBridgeProvider.d.ts +1 -1
- package/dist/lib/gatewayBackend.d.ts +11 -0
- package/dist/lib/gatewayBackend.js +23 -4
- package/dist/lib/mcpConnect.d.ts +6 -3
- package/dist/lib/mcpConnect.js +8 -6
- package/dist/lib/mcpRuntime.d.ts +6 -2
- package/dist/lib/mcpRuntime.js +39 -29
- package/dist/lib/mcpStatusProvider.d.ts +1 -1
- package/dist/lib/memoryProvider.d.ts +1 -1
- package/dist/lib/oauth/discovery.d.ts +1 -0
- package/dist/lib/oauth/discovery.js +2 -0
- package/dist/lib/oauth/flow.d.ts +4 -0
- package/dist/lib/oauth/flow.js +48 -11
- package/dist/lib/oauth/refresh.d.ts +1 -0
- package/dist/lib/oauth/refresh.js +3 -1
- package/dist/lib/oauth/store.d.ts +2 -0
- package/dist/lib/runtimeControlProvider.d.ts +1 -1
- package/dist/lib/skillManifest.d.ts +1 -0
- package/dist/lib/skillManifest.js +1 -1
- package/dist/lib/skillProvider.d.ts +1 -1
- package/dist/lib/taskProvider.d.ts +1 -1
- package/dist/lib/upstreamResources.d.ts +23 -0
- package/dist/lib/upstreamResources.js +192 -0
- package/dist/pi-bridge/bundle.js +37558 -19584
- package/docs/architecture.md +14 -13
- package/docs/mcp-authorization-ownership.md +59 -0
- package/docs/skills-over-mcp.md +43 -31
- package/package.json +5 -2
package/dist/lib/mcpConnect.js
CHANGED
|
@@ -12,10 +12,8 @@
|
|
|
12
12
|
* which servers are in scope, or knows what a tool is; callers isolate
|
|
13
13
|
* failures in whatever way suits them.
|
|
14
14
|
*/
|
|
15
|
-
import { Client } from "@modelcontextprotocol/
|
|
16
|
-
import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/
|
|
17
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
18
|
-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
15
|
+
import { Client, StreamableHTTPClientTransport, SSEClientTransport } from "@modelcontextprotocol/client";
|
|
16
|
+
import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/client/stdio";
|
|
19
17
|
import { resolveSecretEnv } from "./secretEnv.js";
|
|
20
18
|
import { extractTemplateVarNames } from "./envVarNames.js";
|
|
21
19
|
export const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
|
@@ -68,7 +66,7 @@ export async function connectWithCleanup(client, transport, timeoutMs, message)
|
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
export async function connectStdio(def, secretsPolicy, timeoutMs, clientInfo) {
|
|
71
|
-
const client = new Client(clientInfo, { capabilities: {} });
|
|
69
|
+
const client = new Client(clientInfo, { capabilities: {}, versionNegotiation: { mode: "auto", probe: { timeoutMs: Math.min(3000, timeoutMs / 2) } } });
|
|
72
70
|
const envAliases = def.envAliases ?? {};
|
|
73
71
|
// `envAliases`' values are source variable names — resolved through the
|
|
74
72
|
// exact same call as `env`'s own names, never delivered as the raw
|
|
@@ -81,6 +79,9 @@ export async function connectStdio(def, secretsPolicy, timeoutMs, clientInfo) {
|
|
|
81
79
|
const transport = new StdioClientTransport({
|
|
82
80
|
command: def.command,
|
|
83
81
|
args: def.args,
|
|
82
|
+
// A 16 MiB skill file can expand sixfold when JSON escapes control bytes.
|
|
83
|
+
// Keep framing bounded without rejecting a valid Skills-extension file.
|
|
84
|
+
maxBufferSize: 128 * 1024 * 1024,
|
|
84
85
|
// staticEnv merges last: a literal, intentionally-plain value (an
|
|
85
86
|
// email, an environment tag) always wins over an unresolved name-only
|
|
86
87
|
// entry's empty-string fallback for the same key — though in practice
|
|
@@ -102,12 +103,13 @@ export function resolveHeaders(def, secretsPolicy) {
|
|
|
102
103
|
return result;
|
|
103
104
|
}
|
|
104
105
|
export async function connectHttp(def, secretsPolicy, timeoutMs, clientInfo) {
|
|
105
|
-
const client = new Client(clientInfo, { capabilities: {} });
|
|
106
|
+
const client = new Client(clientInfo, { capabilities: {}, versionNegotiation: { mode: "auto", probe: { timeoutMs } } });
|
|
106
107
|
const headers = resolveHeaders(def, secretsPolicy);
|
|
107
108
|
const opts = headers ? { requestInit: { headers } } : undefined;
|
|
108
109
|
return connectWithCleanup(client, new StreamableHTTPClientTransport(new URL(def.url), opts), timeoutMs, `connect timed out after ${timeoutMs}ms`);
|
|
109
110
|
}
|
|
110
111
|
export async function connectSse(def, secretsPolicy, timeoutMs, clientInfo) {
|
|
112
|
+
// HTTP+SSE is a legacy transport; it cannot carry modern discovery.
|
|
111
113
|
const client = new Client(clientInfo, { capabilities: {} });
|
|
112
114
|
const headers = resolveHeaders(def, secretsPolicy);
|
|
113
115
|
const opts = headers ? { requestInit: { headers } } : undefined;
|
package/dist/lib/mcpRuntime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Server } from "@modelcontextprotocol/
|
|
2
|
-
import type { CallToolResult, GetPromptResult, Implementation, Prompt, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/
|
|
1
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
2
|
+
import type { CallToolResult, GetPromptResult, Implementation, Prompt, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/server";
|
|
3
3
|
import type { AgentId } from "../core/types.js";
|
|
4
4
|
import type { GatewayBackend } from "./gatewayBackend.js";
|
|
5
5
|
import type { SkillEntry } from "./skillManifest.js";
|
|
@@ -47,6 +47,10 @@ export declare class UpstreamProvider implements TrellisProvider {
|
|
|
47
47
|
constructor(backend: GatewayBackend);
|
|
48
48
|
listTools(): Promise<Tool[]>;
|
|
49
49
|
callTool(name: string, args: unknown): Promise<CallToolResult>;
|
|
50
|
+
listResources(): Promise<Resource[]>;
|
|
51
|
+
readResource(uri: URL): Promise<ReadResourceResult>;
|
|
52
|
+
listSkillEntries(): Promise<SkillEntry[]>;
|
|
53
|
+
getSkillEntry(uri: string): Promise<SkillEntry | undefined>;
|
|
50
54
|
close(): Promise<void>;
|
|
51
55
|
}
|
|
52
56
|
export declare function createRuntimeServer(serverInfo: Implementation, context: RuntimeContext, registry: BuiltinRegistry): Server;
|
package/dist/lib/mcpRuntime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Server } from "@modelcontextprotocol/
|
|
2
|
-
import {
|
|
1
|
+
import { Server, ProtocolErrorCode, ProtocolError } from "@modelcontextprotocol/server";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
3
|
import { allocateExposedToolNames } from "./mcpToolRegistry.js";
|
|
4
|
+
import { withoutUpstreamServerInfo } from "./upstreamResources.js";
|
|
4
5
|
function compactBuiltinToolName(name) {
|
|
5
6
|
return name.startsWith("trellis.") ? name.slice("trellis.".length).replaceAll(".", "_") : name;
|
|
6
7
|
}
|
|
@@ -102,6 +103,9 @@ export class BuiltinRegistry {
|
|
|
102
103
|
await this.listResources(context);
|
|
103
104
|
provider = this.resourceOwners.get(key);
|
|
104
105
|
}
|
|
106
|
+
// A manifest can introduce supporting files not returned by resources/list.
|
|
107
|
+
if (!provider && uri.protocol === "trellis-upstream:")
|
|
108
|
+
provider = this.providers.find((candidate) => candidate.id === "upstream");
|
|
105
109
|
if (!provider?.readResource)
|
|
106
110
|
throw new Error(`unknown Trellis resource "${key}"`);
|
|
107
111
|
return provider.readResource(uri, context);
|
|
@@ -190,8 +194,16 @@ export class UpstreamProvider {
|
|
|
190
194
|
return (await this.backend.listTools());
|
|
191
195
|
}
|
|
192
196
|
async callTool(name, args) {
|
|
193
|
-
return (await this.backend.callTool(name, args));
|
|
197
|
+
return withoutUpstreamServerInfo((await this.backend.callTool(name, args)));
|
|
194
198
|
}
|
|
199
|
+
async listResources() { return await this.backend.listResources?.() ?? []; }
|
|
200
|
+
async readResource(uri) {
|
|
201
|
+
if (!this.backend.readResource)
|
|
202
|
+
throw new Error("upstream resources unavailable");
|
|
203
|
+
return this.backend.readResource(uri.toString());
|
|
204
|
+
}
|
|
205
|
+
async listSkillEntries() { return await this.backend.listSkills?.() ?? []; }
|
|
206
|
+
async getSkillEntry(uri) { return this.backend.getSkill?.(uri); }
|
|
195
207
|
async close() {
|
|
196
208
|
await this.backend.close();
|
|
197
209
|
}
|
|
@@ -202,35 +214,33 @@ export function createRuntimeServer(serverInfo, context, registry) {
|
|
|
202
214
|
tools: { listChanged: true },
|
|
203
215
|
resources: { listChanged: true },
|
|
204
216
|
prompts: { listChanged: true },
|
|
217
|
+
extensions: { "io.modelcontextprotocol/skills": {} },
|
|
205
218
|
},
|
|
206
219
|
});
|
|
207
|
-
server.setRequestHandler(
|
|
208
|
-
server.setRequestHandler(
|
|
209
|
-
server.setRequestHandler(
|
|
210
|
-
server.setRequestHandler(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// `skills/list` and `skills/get` are not in this SDK's schema set (it speaks
|
|
214
|
-
// at most 2025-11-25), so they are answered through the SDK's fallback for
|
|
215
|
-
// methods with no registered handler, which keeps the extension free of a new
|
|
216
|
-
// direct zod dependency. The extension itself is deliberately NOT declared in
|
|
217
|
-
// the capabilities above: the specification declares it only through
|
|
218
|
-
// `server/discover`, which this revision lacks, and inventing another field
|
|
219
|
-
// would claim a protocol nobody defined (trellis-skills-over-mcp D4). Any
|
|
220
|
-
// other unknown method keeps the SDK's usual "method not found".
|
|
221
|
-
server.fallbackRequestHandler = async (request) => {
|
|
222
|
-
const cache = { resultType: "complete", ttlMs: 0, cacheScope: "private" };
|
|
223
|
-
if (request.method === "skills/list") {
|
|
224
|
-
return { ...cache, skills: await registry.listSkills(context) };
|
|
220
|
+
server.setRequestHandler("tools/list", async () => ({ tools: await registry.listTools(context) }));
|
|
221
|
+
server.setRequestHandler("tools/call", async ({ params }) => registry.callTool(params.name, params.arguments, context));
|
|
222
|
+
server.setRequestHandler("resources/list", async () => ({ resources: await registry.listResources(context) }));
|
|
223
|
+
server.setRequestHandler("resources/read", async ({ params }) => {
|
|
224
|
+
try {
|
|
225
|
+
return await registry.readResource(new URL(params.uri), context);
|
|
225
226
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return { ...cache, skill: entry };
|
|
227
|
+
catch (err) {
|
|
228
|
+
if (err instanceof TypeError || (err instanceof Error && /unknown|not found|not allowed|outside|secret-like|invalid.*uri/i.test(err.message))) {
|
|
229
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "unknown or unreadable resource");
|
|
230
|
+
}
|
|
231
|
+
throw err;
|
|
232
232
|
}
|
|
233
|
-
|
|
234
|
-
};
|
|
233
|
+
});
|
|
234
|
+
server.setRequestHandler("prompts/list", async () => ({ prompts: await registry.listPrompts(context) }));
|
|
235
|
+
server.setRequestHandler("prompts/get", async ({ params }) => registry.getPrompt(params.name, params.arguments, context));
|
|
236
|
+
const result = z.looseObject({});
|
|
237
|
+
const cache = { ttlMs: 0, cacheScope: "private" };
|
|
238
|
+
server.setRequestHandler("skills/list", { params: z.object({ cursor: z.string().optional() }), result }, async () => ({ ...cache, skills: await registry.listSkills(context) }));
|
|
239
|
+
server.setRequestHandler("skills/get", { params: z.object({ uri: z.string() }), result }, async ({ uri }) => {
|
|
240
|
+
const entry = await registry.getSkill(uri, context);
|
|
241
|
+
if (!entry)
|
|
242
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "unknown skill");
|
|
243
|
+
return { ...cache, skill: entry };
|
|
244
|
+
});
|
|
235
245
|
return server;
|
|
236
246
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CallToolResult, Tool } from "@modelcontextprotocol/
|
|
1
|
+
import type { CallToolResult, Tool } from "@modelcontextprotocol/server";
|
|
2
2
|
import type { GatewayBackend } from "./gatewayBackend.js";
|
|
3
3
|
import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
|
|
4
4
|
export declare class McpStatusProvider implements TrellisProvider {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CallToolResult, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/
|
|
1
|
+
import type { CallToolResult, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/server";
|
|
2
2
|
import type { Scope } from "../core/types.js";
|
|
3
3
|
import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
|
|
4
4
|
/**
|
|
@@ -27,6 +27,7 @@ export interface AuthorizationServerMetadata {
|
|
|
27
27
|
registrationEndpoint?: string;
|
|
28
28
|
scopesSupported?: string[];
|
|
29
29
|
codeChallengeMethodsSupported?: string[];
|
|
30
|
+
issuerParameterSupported?: boolean;
|
|
30
31
|
}
|
|
31
32
|
export type FetchLike = (url: string, init?: {
|
|
32
33
|
method?: string;
|
|
@@ -37,6 +37,8 @@ function toMetadata(raw) {
|
|
|
37
37
|
out.scopesSupported = raw.scopes_supported;
|
|
38
38
|
if (raw.code_challenge_methods_supported)
|
|
39
39
|
out.codeChallengeMethodsSupported = raw.code_challenge_methods_supported;
|
|
40
|
+
if (raw.authorization_response_iss_parameter_supported)
|
|
41
|
+
out.issuerParameterSupported = true;
|
|
40
42
|
return out;
|
|
41
43
|
}
|
|
42
44
|
async function tryJson(fetchImpl, url) {
|
package/dist/lib/oauth/flow.d.ts
CHANGED
|
@@ -31,6 +31,9 @@ export interface AuthorizeOptions {
|
|
|
31
31
|
* is actually bound, so a fixed one is never required. */
|
|
32
32
|
callbackPort?: number;
|
|
33
33
|
timeoutMs?: number;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
resourceUrl?: string;
|
|
36
|
+
onProgress?: (phase: "registering" | "waiting" | "exchanging") => void;
|
|
34
37
|
}
|
|
35
38
|
/**
|
|
36
39
|
* RFC 7591. Returns `undefined` when the AS advertises no registration
|
|
@@ -58,3 +61,4 @@ export declare function registerClient(metadata: AuthorizationServerMetadata, re
|
|
|
58
61
|
* caller's decision, so this stays testable without a home directory.
|
|
59
62
|
*/
|
|
60
63
|
export declare function authorize(serverName: string, metadata: AuthorizationServerMetadata, opts?: AuthorizeOptions): Promise<StoredToken>;
|
|
64
|
+
export declare function abortable<T>(value: T | Promise<T>, signal?: AbortSignal): Promise<T>;
|
package/dist/lib/oauth/flow.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { createServer } from "node:http";
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
18
19
|
import { createPkcePair } from "./pkce.js";
|
|
19
20
|
export class AuthorizationError extends Error {
|
|
20
21
|
}
|
|
@@ -89,6 +90,11 @@ function listenForCallback(port) {
|
|
|
89
90
|
});
|
|
90
91
|
const server = createServer((req, res) => {
|
|
91
92
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
93
|
+
if (req.method !== "GET" || url.pathname !== "/callback") {
|
|
94
|
+
res.writeHead(404);
|
|
95
|
+
res.end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
92
98
|
const result = {};
|
|
93
99
|
const code = url.searchParams.get("code");
|
|
94
100
|
const state = url.searchParams.get("state");
|
|
@@ -97,6 +103,9 @@ function listenForCallback(port) {
|
|
|
97
103
|
result.code = code;
|
|
98
104
|
if (state)
|
|
99
105
|
result.state = state;
|
|
106
|
+
const issuer = url.searchParams.get("iss");
|
|
107
|
+
if (issuer)
|
|
108
|
+
result.issuer = issuer;
|
|
100
109
|
if (error) {
|
|
101
110
|
result.error = error;
|
|
102
111
|
const description = url.searchParams.get("error_description");
|
|
@@ -106,7 +115,7 @@ function listenForCallback(port) {
|
|
|
106
115
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
107
116
|
res.end(result.error
|
|
108
117
|
? `<html><body><h1>Authorization failed</h1><p>${escapeHtml(result.errorDescription ?? result.error)}</p></body></html>`
|
|
109
|
-
: "<html><body><h1>
|
|
118
|
+
: "<html><body><h1>Authorization response received</h1><p>You can close this tab and return to Trellis.</p></body></html>");
|
|
110
119
|
settle(result);
|
|
111
120
|
});
|
|
112
121
|
server.on("error", reject);
|
|
@@ -124,7 +133,8 @@ function escapeHtml(value) {
|
|
|
124
133
|
* caller's decision, so this stays testable without a home directory.
|
|
125
134
|
*/
|
|
126
135
|
export async function authorize(serverName, metadata, opts = {}) {
|
|
127
|
-
|
|
136
|
+
opts.signal?.throwIfAborted();
|
|
137
|
+
const fetchImpl = opts.fetchImpl ?? ((url, init) => fetch(url, { ...init, signal: opts.signal }));
|
|
128
138
|
const now = opts.now ?? Date.now;
|
|
129
139
|
const timeoutMs = opts.timeoutMs ?? 300_000;
|
|
130
140
|
const { server, port, received } = await listenForCallback(opts.callbackPort ?? 0);
|
|
@@ -133,13 +143,14 @@ export async function authorize(serverName, metadata, opts = {}) {
|
|
|
133
143
|
let clientId = opts.clientId;
|
|
134
144
|
let clientSecret = opts.clientSecret;
|
|
135
145
|
if (!clientId) {
|
|
146
|
+
opts.onProgress?.("registering");
|
|
136
147
|
// The AS cannot register us AND canonical carries no client_id — the
|
|
137
148
|
// one dead end a person has to resolve by hand, so it is reported as
|
|
138
149
|
// such rather than as a generic registration failure
|
|
139
150
|
// (trellis-mcp-oauth-static-client design.md D3, task 2.3).
|
|
140
151
|
let registered;
|
|
141
152
|
try {
|
|
142
|
-
registered = await registerClient(metadata, redirectUri, { fetchImpl, scope: opts.scope });
|
|
153
|
+
registered = await abortable(registerClient(metadata, redirectUri, { fetchImpl, scope: opts.scope }), opts.signal);
|
|
143
154
|
}
|
|
144
155
|
catch (err) {
|
|
145
156
|
if (err instanceof AuthorizationError) {
|
|
@@ -164,8 +175,12 @@ export async function authorize(serverName, metadata, opts = {}) {
|
|
|
164
175
|
authUrl.searchParams.set("state", state);
|
|
165
176
|
if (opts.scope)
|
|
166
177
|
authUrl.searchParams.set("scope", opts.scope);
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
if (opts.resourceUrl)
|
|
179
|
+
authUrl.searchParams.set("resource", opts.resourceUrl);
|
|
180
|
+
opts.signal?.throwIfAborted();
|
|
181
|
+
opts.onProgress?.("waiting");
|
|
182
|
+
await abortable((opts.openBrowser ?? defaultOpenBrowser)(authUrl.toString()), opts.signal);
|
|
183
|
+
const callback = await withTimeout(received, timeoutMs, `timed out waiting for the authorization callback for "${serverName}"`, opts.signal);
|
|
169
184
|
if (callback.error) {
|
|
170
185
|
throw new AuthorizationError(`authorization for "${serverName}" was refused: ${callback.errorDescription ?? callback.error}`);
|
|
171
186
|
}
|
|
@@ -177,6 +192,11 @@ export async function authorize(serverName, metadata, opts = {}) {
|
|
|
177
192
|
if (callback.state !== state) {
|
|
178
193
|
throw new AuthorizationError(`authorization callback for "${serverName}" carried an unexpected state parameter`);
|
|
179
194
|
}
|
|
195
|
+
if ((callback.issuer && callback.issuer !== metadata.issuer) || (metadata.issuerParameterSupported && !callback.issuer)) {
|
|
196
|
+
throw new AuthorizationError("authorization callback issuer does not match the discovered authorization server");
|
|
197
|
+
}
|
|
198
|
+
opts.signal?.throwIfAborted();
|
|
199
|
+
opts.onProgress?.("exchanging");
|
|
180
200
|
const body = new URLSearchParams({
|
|
181
201
|
grant_type: "authorization_code",
|
|
182
202
|
code: callback.code,
|
|
@@ -186,11 +206,13 @@ export async function authorize(serverName, metadata, opts = {}) {
|
|
|
186
206
|
});
|
|
187
207
|
if (clientSecret)
|
|
188
208
|
body.set("client_secret", clientSecret);
|
|
189
|
-
|
|
209
|
+
if (opts.resourceUrl)
|
|
210
|
+
body.set("resource", opts.resourceUrl);
|
|
211
|
+
const response = await abortable(fetchImpl(metadata.tokenEndpoint, {
|
|
190
212
|
method: "POST",
|
|
191
213
|
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
192
214
|
body: body.toString(),
|
|
193
|
-
});
|
|
215
|
+
}), opts.signal);
|
|
194
216
|
if (!response.ok) {
|
|
195
217
|
throw new AuthorizationError(`token exchange for "${serverName}" failed with HTTP ${response.status}`);
|
|
196
218
|
}
|
|
@@ -202,6 +224,8 @@ export async function authorize(serverName, metadata, opts = {}) {
|
|
|
202
224
|
accessToken: payload.access_token,
|
|
203
225
|
tokenEndpoint: metadata.tokenEndpoint,
|
|
204
226
|
clientId,
|
|
227
|
+
issuer: metadata.issuer,
|
|
228
|
+
...(opts.resourceUrl ? { resourceUrl: opts.resourceUrl } : {}),
|
|
205
229
|
};
|
|
206
230
|
if (payload.refresh_token)
|
|
207
231
|
token.refreshToken = payload.refresh_token;
|
|
@@ -214,25 +238,38 @@ export async function authorize(serverName, metadata, opts = {}) {
|
|
|
214
238
|
return token;
|
|
215
239
|
}
|
|
216
240
|
finally {
|
|
241
|
+
server.closeAllConnections();
|
|
217
242
|
server.close();
|
|
218
243
|
}
|
|
219
244
|
}
|
|
220
|
-
function withTimeout(promise, timeoutMs, message) {
|
|
245
|
+
function withTimeout(promise, timeoutMs, message, signal) {
|
|
221
246
|
return new Promise((resolve, reject) => {
|
|
222
|
-
const
|
|
247
|
+
const abort = () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); reject(signal?.reason ?? new Error("authorization cancelled")); };
|
|
248
|
+
const timer = setTimeout(() => { signal?.removeEventListener("abort", abort); reject(new AuthorizationError(message)); }, timeoutMs);
|
|
249
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
250
|
+
if (signal?.aborted)
|
|
251
|
+
abort();
|
|
223
252
|
promise.then((value) => {
|
|
224
253
|
clearTimeout(timer);
|
|
254
|
+
signal?.removeEventListener("abort", abort);
|
|
225
255
|
resolve(value);
|
|
226
256
|
}, (err) => {
|
|
227
257
|
clearTimeout(timer);
|
|
258
|
+
signal?.removeEventListener("abort", abort);
|
|
228
259
|
reject(err);
|
|
229
260
|
});
|
|
230
261
|
});
|
|
231
262
|
}
|
|
263
|
+
export function abortable(value, signal) {
|
|
264
|
+
return withTimeout(Promise.resolve(value), 300_000, "authorization timed out", signal);
|
|
265
|
+
}
|
|
232
266
|
async function defaultOpenBrowser(url) {
|
|
233
|
-
const { spawn } = await import("node:child_process");
|
|
234
267
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
235
268
|
// Detached and fully ignored: a browser that outlives this command is
|
|
236
269
|
// correct, and its stdio must not be inherited onto ours.
|
|
237
|
-
|
|
270
|
+
await new Promise((resolve, reject) => {
|
|
271
|
+
const child = spawn(command, [url], { detached: true, stdio: "ignore" });
|
|
272
|
+
child.once("error", reject);
|
|
273
|
+
child.once("spawn", () => { child.unref(); resolve(); });
|
|
274
|
+
});
|
|
238
275
|
}
|
|
@@ -12,6 +12,7 @@ import { type StoredToken } from "./store.js";
|
|
|
12
12
|
import type { FetchLike } from "./discovery.js";
|
|
13
13
|
export interface RefreshOptions {
|
|
14
14
|
fetchImpl?: FetchLike;
|
|
15
|
+
timeoutMs?: number;
|
|
15
16
|
/** Test seam; also lets a caller widen the early-refresh skew. */
|
|
16
17
|
now?: () => number;
|
|
17
18
|
skewMs?: number;
|
|
@@ -49,7 +49,7 @@ export async function ensureFreshToken(homeDir, serverName, opts = {}) {
|
|
|
49
49
|
}, { timeoutMs: opts.lockTimeoutMs, now });
|
|
50
50
|
}
|
|
51
51
|
export async function performRefresh(serverName, token, opts = {}) {
|
|
52
|
-
const fetchImpl = opts.fetchImpl ??
|
|
52
|
+
const fetchImpl = opts.fetchImpl ?? ((url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(opts.timeoutMs ?? 10_000) }));
|
|
53
53
|
const now = opts.now ?? Date.now;
|
|
54
54
|
if (!token.tokenEndpoint) {
|
|
55
55
|
throw new RefreshError(`OAuth token for "${serverName}" has no recorded token endpoint — run \`trellis mcp auth ${serverName}\``);
|
|
@@ -59,6 +59,8 @@ export async function performRefresh(serverName, token, opts = {}) {
|
|
|
59
59
|
body.set("client_id", token.clientId);
|
|
60
60
|
if (token.clientSecret)
|
|
61
61
|
body.set("client_secret", token.clientSecret);
|
|
62
|
+
if (token.resourceUrl)
|
|
63
|
+
body.set("resource", token.resourceUrl);
|
|
62
64
|
const response = await fetchImpl(token.tokenEndpoint, {
|
|
63
65
|
method: "POST",
|
|
64
66
|
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
@@ -29,6 +29,8 @@ export interface StoredToken {
|
|
|
29
29
|
* metadata — the gateway's refresh path should be one request. */
|
|
30
30
|
tokenEndpoint?: string;
|
|
31
31
|
scope?: string;
|
|
32
|
+
resourceUrl?: string;
|
|
33
|
+
issuer?: string;
|
|
32
34
|
}
|
|
33
35
|
export declare function oauthDir(homeDir: string): string;
|
|
34
36
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CallToolResult, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/
|
|
1
|
+
import type { CallToolResult, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/server";
|
|
2
2
|
import type { GatewayBackend } from "./gatewayBackend.js";
|
|
3
3
|
import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
|
|
4
4
|
export declare class RuntimeControlProvider implements TrellisProvider {
|
|
@@ -94,7 +94,7 @@ export function mimeTypeFor(path) {
|
|
|
94
94
|
* base64 blob, or the bytes a host verifies would not be the bytes we hashed. */
|
|
95
95
|
export function toResourceContent(uri, path, bytes) {
|
|
96
96
|
try {
|
|
97
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
97
|
+
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
98
98
|
return { uri, mimeType: mimeTypeFor(path), text };
|
|
99
99
|
}
|
|
100
100
|
catch {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Resource, ReadResourceResult, Tool, CallToolResult } from "@modelcontextprotocol/
|
|
1
|
+
import type { Resource, ReadResourceResult, Tool, CallToolResult } from "@modelcontextprotocol/server";
|
|
2
2
|
import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
|
|
3
3
|
import { type SkillEntry } from "./skillManifest.js";
|
|
4
4
|
export declare class SkillProvider implements TrellisProvider {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CallToolResult, Tool } from "@modelcontextprotocol/
|
|
1
|
+
import type { CallToolResult, Tool } from "@modelcontextprotocol/server";
|
|
2
2
|
import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
|
|
3
3
|
export declare class TaskProvider implements TrellisProvider {
|
|
4
4
|
readonly id = "tasks";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Client, Resource, ReadResourceResult } from "@modelcontextprotocol/client";
|
|
2
|
+
import { type SkillEntry } from "./skillManifest.js";
|
|
3
|
+
/** The outer response belongs to the gateway; upstream identity is provenance. */
|
|
4
|
+
export declare function withoutUpstreamServerInfo<T extends {
|
|
5
|
+
_meta?: Record<string, unknown>;
|
|
6
|
+
}>(result: T): T;
|
|
7
|
+
/** Retain path segments so relative supporting-file references remain valid. */
|
|
8
|
+
export declare function upstreamUri(source: string, uri: string): string;
|
|
9
|
+
export declare class UpstreamResources {
|
|
10
|
+
private readonly timeoutMs;
|
|
11
|
+
private readonly warn;
|
|
12
|
+
private readonly clients;
|
|
13
|
+
private readonly files;
|
|
14
|
+
constructor(timeoutMs: number, warn: (message: string) => void);
|
|
15
|
+
add(source: string, client: Client): void;
|
|
16
|
+
private skillsEnabled;
|
|
17
|
+
private isolated;
|
|
18
|
+
listResources(): Promise<Resource[]>;
|
|
19
|
+
private registerSkill;
|
|
20
|
+
listSkills(): Promise<SkillEntry[]>;
|
|
21
|
+
getSkill(uri: string): Promise<SkillEntry | undefined>;
|
|
22
|
+
readResource(uri: string): Promise<ReadResourceResult>;
|
|
23
|
+
}
|