toolcraft 0.0.5 → 0.0.7
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/README.md +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +77 -59
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +15 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +13 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/codex.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +14 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/goose.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +14 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +7 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/index.js +7 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/kimi.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +15 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/opencode.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +14 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +13 -0
- package/node_modules/@poe-code/agent-defs/dist/index.d.ts +5 -0
- package/node_modules/@poe-code/agent-defs/dist/index.js +3 -0
- package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +3 -0
- package/node_modules/@poe-code/agent-defs/dist/registry.js +26 -0
- package/node_modules/@poe-code/agent-defs/dist/specifier.d.ts +7 -0
- package/node_modules/@poe-code/agent-defs/dist/specifier.js +27 -0
- package/node_modules/@poe-code/agent-defs/dist/types.d.ts +16 -0
- package/node_modules/@poe-code/agent-defs/dist/types.js +1 -0
- package/node_modules/@poe-code/agent-defs/package.json +20 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +5 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +552 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.d.ts +17 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +58 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.d.ts +7 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +46 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/index.d.ts +13 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/index.js +49 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +31 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +140 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.d.ts +2 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +72 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.d.ts +2 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +73 -0
- package/node_modules/@poe-code/config-mutations/dist/fs-utils.d.ts +18 -0
- package/node_modules/@poe-code/config-mutations/dist/fs-utils.js +45 -0
- package/node_modules/@poe-code/config-mutations/dist/index.d.ts +8 -0
- package/node_modules/@poe-code/config-mutations/dist/index.js +8 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.d.ts +47 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.js +34 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +52 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +46 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.d.ts +40 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.js +32 -0
- package/node_modules/@poe-code/config-mutations/dist/template/render.d.ts +7 -0
- package/node_modules/@poe-code/config-mutations/dist/template/render.js +28 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.d.ts +7 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.js +21 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/index.d.ts +3 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/index.js +2 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.d.ts +25 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +170 -0
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +156 -0
- package/node_modules/@poe-code/config-mutations/dist/types.js +6 -0
- package/node_modules/@poe-code/config-mutations/package.json +33 -0
- package/node_modules/@poe-code/file-lock/README.md +52 -0
- package/node_modules/@poe-code/file-lock/dist/index.d.ts +1 -0
- package/node_modules/@poe-code/file-lock/dist/index.js +1 -0
- package/node_modules/@poe-code/file-lock/dist/lock.d.ts +27 -0
- package/node_modules/@poe-code/file-lock/dist/lock.js +203 -0
- package/node_modules/@poe-code/file-lock/package.json +23 -0
- package/node_modules/auth-store/README.md +47 -0
- package/node_modules/auth-store/dist/create-secret-store.d.ts +2 -0
- package/node_modules/auth-store/dist/create-secret-store.js +35 -0
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +39 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +156 -0
- package/node_modules/auth-store/dist/index.d.ts +7 -0
- package/node_modules/auth-store/dist/index.js +4 -0
- package/node_modules/auth-store/dist/keychain-store.d.ts +22 -0
- package/node_modules/auth-store/dist/keychain-store.js +111 -0
- package/node_modules/auth-store/dist/provider-store.d.ts +10 -0
- package/node_modules/auth-store/dist/provider-store.js +28 -0
- package/node_modules/auth-store/dist/types.d.ts +20 -0
- package/node_modules/auth-store/dist/types.js +1 -0
- package/node_modules/auth-store/package.json +25 -0
- package/node_modules/mcp-oauth/README.md +31 -0
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +97 -0
- package/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
- package/node_modules/mcp-oauth/dist/client/authorization-state.js +34 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +491 -0
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +169 -0
- package/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
- package/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +143 -0
- package/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
- package/node_modules/mcp-oauth/dist/client/types.js +1 -0
- package/node_modules/mcp-oauth/dist/index.d.ts +10 -0
- package/node_modules/mcp-oauth/dist/index.js +7 -0
- package/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +27 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +259 -0
- package/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
- package/node_modules/mcp-oauth/package.json +31 -0
- package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +4 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +2 -0
- package/node_modules/tiny-mcp-client/dist/index.js +1 -0
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +547 -0
- package/node_modules/tiny-mcp-client/dist/internal.js +2404 -0
- package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.js +37 -0
- package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.js +50 -0
- package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.js +50 -0
- package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.js +51 -0
- package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.js +89 -0
- package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.js +56 -0
- package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.js +145 -0
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +24 -0
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +385 -0
- package/node_modules/tiny-mcp-client/package.json +22 -0
- package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +823 -0
- package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +882 -0
- package/node_modules/tiny-mcp-client/src/index.ts +94 -0
- package/node_modules/tiny-mcp-client/src/internal.ts +3566 -0
- package/node_modules/tiny-mcp-client/src/jsonrpc-types.compile-check.ts +66 -0
- package/node_modules/tiny-mcp-client/src/mcp-client-http-transport.integration.test.ts +222 -0
- package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +1294 -0
- package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +143 -0
- package/node_modules/tiny-mcp-client/src/mcp-lifecycle-types.compile-check.ts +65 -0
- package/node_modules/tiny-mcp-client/src/mcp-prompt-types.compile-check.ts +66 -0
- package/node_modules/tiny-mcp-client/src/mcp-resource-types.compile-check.ts +70 -0
- package/node_modules/tiny-mcp-client/src/mcp-tool-types.compile-check.ts +117 -0
- package/node_modules/tiny-mcp-client/src/mcp-transport-types.compile-check.ts +75 -0
- package/node_modules/tiny-mcp-client/src/mcp-utility-types.compile-check.ts +181 -0
- package/node_modules/tiny-mcp-client/src/mock-servers.test.ts +980 -0
- package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +583 -0
- package/node_modules/tiny-mcp-client/src/transports.test.ts +8139 -0
- package/node_modules/tiny-mcp-client/src/utilities.test.ts +372 -0
- package/node_modules/tiny-mcp-client/tsconfig.json +11 -0
- package/package.json +24 -11
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
OAuthError,
|
|
5
|
+
type OAuthSessionStore,
|
|
6
|
+
type StoredOAuthSession,
|
|
7
|
+
} from "mcp-oauth";
|
|
8
|
+
import { nodeFetch } from "tiny-http-mcp-server/testing";
|
|
9
|
+
import {
|
|
10
|
+
createMcpOAuthTestServer,
|
|
11
|
+
type McpOAuthTestServerHandle,
|
|
12
|
+
type McpOAuthTestServerOptions,
|
|
13
|
+
} from "tiny-http-mcp-oauth-test-server";
|
|
14
|
+
import {
|
|
15
|
+
HttpTransport,
|
|
16
|
+
McpClient,
|
|
17
|
+
resolveAuthorizationServerMetadataUrl,
|
|
18
|
+
} from "./internal.js";
|
|
19
|
+
|
|
20
|
+
interface RequestRecord {
|
|
21
|
+
url: string;
|
|
22
|
+
method: string;
|
|
23
|
+
authorization: string | null;
|
|
24
|
+
sessionId: string | null;
|
|
25
|
+
body: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SessionStoreWithMap {
|
|
29
|
+
sessions: Map<string, StoredOAuthSession>;
|
|
30
|
+
store: OAuthSessionStore;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface OAuthClientHarness {
|
|
34
|
+
client: McpClient;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
handle: McpOAuthTestServerHandle;
|
|
37
|
+
requests: RequestRecord[];
|
|
38
|
+
sessionStore: SessionStoreWithMap;
|
|
39
|
+
setNow(value: number): void;
|
|
40
|
+
transport: HttpTransport;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface TrafficSummary {
|
|
44
|
+
authorize: number;
|
|
45
|
+
asMetadata: number;
|
|
46
|
+
mcpPost: number;
|
|
47
|
+
prm: number;
|
|
48
|
+
register: number;
|
|
49
|
+
tokenAuthorizationCode: number;
|
|
50
|
+
tokenRefresh: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface AuthorizationServerTrafficSummary {
|
|
54
|
+
authorize: number;
|
|
55
|
+
metadata: number;
|
|
56
|
+
register: number;
|
|
57
|
+
tokenAuthorizationCode: number;
|
|
58
|
+
tokenRefresh: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createSessionStore(): SessionStoreWithMap {
|
|
62
|
+
const sessions = new Map<string, StoredOAuthSession>();
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
sessions,
|
|
66
|
+
store: {
|
|
67
|
+
async load(resource: string): Promise<StoredOAuthSession | null> {
|
|
68
|
+
return sessions.get(resource) ?? null;
|
|
69
|
+
},
|
|
70
|
+
async save(resource: string, session: StoredOAuthSession): Promise<void> {
|
|
71
|
+
sessions.set(resource, session);
|
|
72
|
+
},
|
|
73
|
+
async clear(resource: string): Promise<void> {
|
|
74
|
+
sessions.delete(resource);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cloneResponse(response: Response, body: string): Response {
|
|
81
|
+
const headers = new Headers(response.headers);
|
|
82
|
+
return new Response(body, {
|
|
83
|
+
status: response.status,
|
|
84
|
+
statusText: response.statusText,
|
|
85
|
+
headers,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function reservePort(hostname: string): Promise<number> {
|
|
90
|
+
const server = http.createServer();
|
|
91
|
+
|
|
92
|
+
await new Promise<void>((resolve, reject) => {
|
|
93
|
+
server.once("error", reject);
|
|
94
|
+
server.listen(0, hostname, () => resolve());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const address = server.address();
|
|
98
|
+
if (address === null || typeof address === "string") {
|
|
99
|
+
await new Promise<void>((resolve, reject) => {
|
|
100
|
+
server.close((error) => {
|
|
101
|
+
if (error !== undefined) {
|
|
102
|
+
reject(error);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
throw new Error("Expected temporary port reservation to bind to a TCP port");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const port = address.port;
|
|
113
|
+
await new Promise<void>((resolve, reject) => {
|
|
114
|
+
server.close((error) => {
|
|
115
|
+
if (error !== undefined) {
|
|
116
|
+
reject(error);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
resolve();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
return port;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createFixedPortServerFactory(port: number): () => http.Server {
|
|
127
|
+
return () => {
|
|
128
|
+
const server = http.createServer();
|
|
129
|
+
const originalListen = server.listen.bind(server);
|
|
130
|
+
|
|
131
|
+
server.listen = ((...args: unknown[]) => {
|
|
132
|
+
let callback: (() => void) | undefined;
|
|
133
|
+
|
|
134
|
+
for (let index = args.length - 1; index >= 0; index -= 1) {
|
|
135
|
+
const value = args[index];
|
|
136
|
+
if (typeof value === "function") {
|
|
137
|
+
callback = value as () => void;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return originalListen(port, "127.0.0.1", callback);
|
|
143
|
+
}) as typeof server.listen;
|
|
144
|
+
|
|
145
|
+
return server;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseJsonBody(record: RequestRecord): Record<string, unknown> {
|
|
150
|
+
if (record.body === undefined) {
|
|
151
|
+
throw new Error(`Expected JSON body for ${record.method} ${record.url}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return JSON.parse(record.body) as Record<string, unknown>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseFormBody(record: RequestRecord): URLSearchParams {
|
|
158
|
+
return new URLSearchParams(record.body ?? "");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function requireRequest(
|
|
162
|
+
request: RequestRecord | undefined,
|
|
163
|
+
description: string
|
|
164
|
+
): RequestRecord {
|
|
165
|
+
if (request === undefined) {
|
|
166
|
+
throw new Error(`Expected ${description}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return request;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function findLastRequest(
|
|
173
|
+
requests: readonly RequestRecord[],
|
|
174
|
+
predicate: (request: RequestRecord) => boolean,
|
|
175
|
+
description: string
|
|
176
|
+
): RequestRecord {
|
|
177
|
+
for (let index = requests.length - 1; index >= 0; index -= 1) {
|
|
178
|
+
const request = requests[index];
|
|
179
|
+
if (request !== undefined && predicate(request)) {
|
|
180
|
+
return request;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw new Error(`Expected ${description}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function summarizeTraffic(
|
|
188
|
+
requests: readonly RequestRecord[],
|
|
189
|
+
handle: McpOAuthTestServerHandle
|
|
190
|
+
): TrafficSummary {
|
|
191
|
+
const authorizationServerMetadataUrl = resolveAuthorizationServerMetadataUrl(handle.oauth.issuer);
|
|
192
|
+
const authorizeUrl = `${handle.oauth.issuer}/authorize`;
|
|
193
|
+
const registerUrl = `${handle.oauth.issuer}/register`;
|
|
194
|
+
const tokenUrl = `${handle.oauth.issuer}/token`;
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
prm: requests.filter(
|
|
198
|
+
(request) => request.method === "GET" && request.url === handle.prmUrl
|
|
199
|
+
).length,
|
|
200
|
+
asMetadata: requests.filter(
|
|
201
|
+
(request) =>
|
|
202
|
+
request.method === "GET"
|
|
203
|
+
&& request.url === authorizationServerMetadataUrl.toString()
|
|
204
|
+
).length,
|
|
205
|
+
register: requests.filter(
|
|
206
|
+
(request) => request.method === "POST" && request.url === registerUrl
|
|
207
|
+
).length,
|
|
208
|
+
authorize: requests.filter(
|
|
209
|
+
(request) => request.method === "GET" && request.url.startsWith(authorizeUrl)
|
|
210
|
+
).length,
|
|
211
|
+
tokenAuthorizationCode: requests.filter((request) => {
|
|
212
|
+
if (request.method !== "POST" || request.url !== tokenUrl) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return parseFormBody(request).get("grant_type") === "authorization_code";
|
|
217
|
+
}).length,
|
|
218
|
+
tokenRefresh: requests.filter((request) => {
|
|
219
|
+
if (request.method !== "POST" || request.url !== tokenUrl) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return parseFormBody(request).get("grant_type") === "refresh_token";
|
|
224
|
+
}).length,
|
|
225
|
+
mcpPost: requests.filter(
|
|
226
|
+
(request) => request.method === "POST" && request.url === handle.mcpUrl
|
|
227
|
+
).length,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function summarizeAuthorizationServerTraffic(
|
|
232
|
+
handle: McpOAuthTestServerHandle
|
|
233
|
+
): AuthorizationServerTrafficSummary {
|
|
234
|
+
const requestLog = handle.oauth.requestLog;
|
|
235
|
+
const authorizationServerMetadataUrl = resolveAuthorizationServerMetadataUrl(handle.oauth.issuer);
|
|
236
|
+
const authorizeUrl = `${handle.oauth.issuer}/authorize`;
|
|
237
|
+
const registerUrl = `${handle.oauth.issuer}/register`;
|
|
238
|
+
const tokenUrl = `${handle.oauth.issuer}/token`;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
metadata: requestLog.filter(
|
|
242
|
+
(request) =>
|
|
243
|
+
request.method === "GET"
|
|
244
|
+
&& request.url === authorizationServerMetadataUrl.toString()
|
|
245
|
+
).length,
|
|
246
|
+
register: requestLog.filter(
|
|
247
|
+
(request) => request.method === "POST" && request.url === registerUrl
|
|
248
|
+
).length,
|
|
249
|
+
authorize: requestLog.filter(
|
|
250
|
+
(request) => request.method === "GET" && request.url.startsWith(authorizeUrl)
|
|
251
|
+
).length,
|
|
252
|
+
tokenAuthorizationCode: requestLog.filter((request) => {
|
|
253
|
+
if (request.method !== "POST" || request.url !== tokenUrl) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return new URLSearchParams(request.body ?? "").get("grant_type") === "authorization_code";
|
|
258
|
+
}).length,
|
|
259
|
+
tokenRefresh: requestLog.filter((request) => {
|
|
260
|
+
if (request.method !== "POST" || request.url !== tokenUrl) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return new URLSearchParams(request.body ?? "").get("grant_type") === "refresh_token";
|
|
265
|
+
}).length,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getJsonRpcMethod(record: RequestRecord): string | undefined {
|
|
270
|
+
if (record.body === undefined) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(record.body) as { method?: unknown };
|
|
276
|
+
return typeof parsed.method === "string" ? parsed.method : undefined;
|
|
277
|
+
} catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getTextContent(result: unknown): string | undefined {
|
|
283
|
+
if (
|
|
284
|
+
typeof result !== "object"
|
|
285
|
+
|| result === null
|
|
286
|
+
|| !("content" in result)
|
|
287
|
+
|| !Array.isArray(result.content)
|
|
288
|
+
) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const [firstItem] = result.content;
|
|
293
|
+
if (
|
|
294
|
+
typeof firstItem !== "object"
|
|
295
|
+
|| firstItem === null
|
|
296
|
+
|| !("text" in firstItem)
|
|
297
|
+
|| typeof firstItem.text !== "string"
|
|
298
|
+
) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return firstItem.text;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function getStoredSession(harness: OAuthClientHarness): StoredOAuthSession {
|
|
306
|
+
const session = harness.sessionStore.sessions.get(harness.handle.mcpUrl);
|
|
307
|
+
if (session === undefined) {
|
|
308
|
+
throw new Error("Expected OAuth session to be stored");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return session;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function createHarness(options: {
|
|
315
|
+
now?: () => number;
|
|
316
|
+
oauthClient?:
|
|
317
|
+
| {
|
|
318
|
+
mode: "dynamic";
|
|
319
|
+
metadata?: {
|
|
320
|
+
clientName?: string;
|
|
321
|
+
scope?: string;
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
| {
|
|
325
|
+
mode: "static";
|
|
326
|
+
clientId: string;
|
|
327
|
+
clientSecret?: string;
|
|
328
|
+
metadata?: {
|
|
329
|
+
clientName?: string;
|
|
330
|
+
scope?: string;
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
responseTransform?: (input: {
|
|
334
|
+
handle: McpOAuthTestServerHandle;
|
|
335
|
+
record: RequestRecord;
|
|
336
|
+
response: Response;
|
|
337
|
+
}) => Promise<Response | undefined>;
|
|
338
|
+
serverOptions?: McpOAuthTestServerOptions;
|
|
339
|
+
createServer?: () => http.Server;
|
|
340
|
+
} = {}): Promise<OAuthClientHarness> {
|
|
341
|
+
const server = createMcpOAuthTestServer({
|
|
342
|
+
autoApprove: true,
|
|
343
|
+
scopes: ["mcp.read"],
|
|
344
|
+
...(options.serverOptions ?? {}),
|
|
345
|
+
});
|
|
346
|
+
const handle = await server.listen({
|
|
347
|
+
port: 0,
|
|
348
|
+
hostname: "127.0.0.1",
|
|
349
|
+
});
|
|
350
|
+
const requests: RequestRecord[] = [];
|
|
351
|
+
const sessionStore = createSessionStore();
|
|
352
|
+
let currentNow = options.now?.() ?? 10_000;
|
|
353
|
+
const fetchImpl = async (input: string | URL, init: RequestInit = {}): Promise<Response> => {
|
|
354
|
+
const record: RequestRecord = {
|
|
355
|
+
url: input.toString(),
|
|
356
|
+
method: init.method ?? "GET",
|
|
357
|
+
authorization: new Headers(init.headers).get("authorization"),
|
|
358
|
+
sessionId: new Headers(init.headers).get("mcp-session-id"),
|
|
359
|
+
body: typeof init.body === "string" ? init.body : undefined,
|
|
360
|
+
};
|
|
361
|
+
const response = await nodeFetch(record.url, init);
|
|
362
|
+
const transformed = options.responseTransform === undefined
|
|
363
|
+
? undefined
|
|
364
|
+
: await options.responseTransform({
|
|
365
|
+
handle,
|
|
366
|
+
record,
|
|
367
|
+
response,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
requests.push(record);
|
|
371
|
+
return transformed ?? response;
|
|
372
|
+
};
|
|
373
|
+
const client = new McpClient({
|
|
374
|
+
clientInfo: {
|
|
375
|
+
name: "tiny-mcp-client-http-oauth-integration-test",
|
|
376
|
+
version: "1.0.0",
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
const transport = new HttpTransport({
|
|
380
|
+
url: handle.mcpUrl,
|
|
381
|
+
fetch: fetchImpl,
|
|
382
|
+
oauth: {
|
|
383
|
+
client: options.oauthClient ?? {
|
|
384
|
+
mode: "dynamic",
|
|
385
|
+
metadata: {
|
|
386
|
+
clientName: "tiny-mcp-client integration test",
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
browser: {
|
|
390
|
+
async openBrowser(authorizationUrl) {
|
|
391
|
+
const authorizationResponse = await fetchImpl(authorizationUrl, {
|
|
392
|
+
method: "GET",
|
|
393
|
+
});
|
|
394
|
+
if (authorizationResponse.status !== 302) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Expected authorization redirect, received ${authorizationResponse.status}: ${await authorizationResponse.text()}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const callbackUrl = authorizationResponse.headers.get("location");
|
|
401
|
+
expect(callbackUrl).toBeTruthy();
|
|
402
|
+
|
|
403
|
+
const callbackResponse = await fetchImpl(callbackUrl ?? "", {
|
|
404
|
+
method: "GET",
|
|
405
|
+
});
|
|
406
|
+
expect(callbackResponse.ok).toBe(true);
|
|
407
|
+
await callbackResponse.text();
|
|
408
|
+
},
|
|
409
|
+
...(options.createServer === undefined
|
|
410
|
+
? {}
|
|
411
|
+
: {
|
|
412
|
+
createServer: options.createServer,
|
|
413
|
+
}),
|
|
414
|
+
},
|
|
415
|
+
now: () => currentNow,
|
|
416
|
+
sessionStore: sessionStore.store,
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
client,
|
|
422
|
+
close: async () => {
|
|
423
|
+
await client.close().catch(() => undefined);
|
|
424
|
+
await handle.close();
|
|
425
|
+
},
|
|
426
|
+
handle,
|
|
427
|
+
requests,
|
|
428
|
+
sessionStore,
|
|
429
|
+
setNow(value: number): void {
|
|
430
|
+
currentNow = value;
|
|
431
|
+
},
|
|
432
|
+
transport,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
describe("HttpTransport OAuth integration", () => {
|
|
437
|
+
const cleanups = new Set<() => Promise<void>>();
|
|
438
|
+
|
|
439
|
+
afterEach(async () => {
|
|
440
|
+
for (const cleanup of [...cleanups].reverse()) {
|
|
441
|
+
await cleanup();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
cleanups.clear();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("runs discovery, DCR, PKCE authorization, attaches the bearer, and reuses the cached token", async () => {
|
|
448
|
+
const harness = await createHarness();
|
|
449
|
+
cleanups.add(harness.close);
|
|
450
|
+
|
|
451
|
+
await harness.client.connect(harness.transport);
|
|
452
|
+
|
|
453
|
+
const firstResult = await harness.client.callTool({
|
|
454
|
+
name: "echo",
|
|
455
|
+
arguments: {
|
|
456
|
+
text: "first-call",
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(getTextContent(firstResult)).toBe("first-call");
|
|
461
|
+
|
|
462
|
+
const summaryAfterFirstCall = summarizeTraffic(harness.requests, harness.handle);
|
|
463
|
+
expect(summaryAfterFirstCall).toEqual({
|
|
464
|
+
authorize: 1,
|
|
465
|
+
asMetadata: 1,
|
|
466
|
+
mcpPost: 4,
|
|
467
|
+
prm: 1,
|
|
468
|
+
register: 1,
|
|
469
|
+
tokenAuthorizationCode: 1,
|
|
470
|
+
tokenRefresh: 0,
|
|
471
|
+
});
|
|
472
|
+
expect(summarizeAuthorizationServerTraffic(harness.handle)).toEqual({
|
|
473
|
+
authorize: 1,
|
|
474
|
+
metadata: 1,
|
|
475
|
+
register: 1,
|
|
476
|
+
tokenAuthorizationCode: 1,
|
|
477
|
+
tokenRefresh: 0,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const initializePosts = harness.requests.filter(
|
|
481
|
+
(request) =>
|
|
482
|
+
request.method === "POST"
|
|
483
|
+
&& request.url === harness.handle.mcpUrl
|
|
484
|
+
&& getJsonRpcMethod(request) === "initialize"
|
|
485
|
+
);
|
|
486
|
+
expect(initializePosts).toHaveLength(2);
|
|
487
|
+
expect(initializePosts[0]?.authorization).toBeNull();
|
|
488
|
+
expect(initializePosts[1]?.authorization).toMatch(/^Bearer /);
|
|
489
|
+
|
|
490
|
+
const callToolPosts = harness.requests.filter(
|
|
491
|
+
(request) =>
|
|
492
|
+
request.method === "POST"
|
|
493
|
+
&& request.url === harness.handle.mcpUrl
|
|
494
|
+
&& getJsonRpcMethod(request) === "tools/call"
|
|
495
|
+
);
|
|
496
|
+
expect(callToolPosts).toHaveLength(1);
|
|
497
|
+
expect(callToolPosts[0]?.authorization).toMatch(/^Bearer /);
|
|
498
|
+
|
|
499
|
+
const registrationRequest = harness.requests.find(
|
|
500
|
+
(request) => request.method === "POST" && request.url === `${harness.handle.oauth.issuer}/register`
|
|
501
|
+
);
|
|
502
|
+
expect(parseJsonBody(requireRequest(registrationRequest, "registration request"))).toMatchObject({
|
|
503
|
+
client_name: "tiny-mcp-client integration test",
|
|
504
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
505
|
+
response_types: ["code"],
|
|
506
|
+
token_endpoint_auth_method: "none",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const authorizationRequest = harness.requests.find(
|
|
510
|
+
(request) =>
|
|
511
|
+
request.method === "GET"
|
|
512
|
+
&& request.url.startsWith(`${harness.handle.oauth.issuer}/authorize`)
|
|
513
|
+
);
|
|
514
|
+
expect(new URL(requireRequest(authorizationRequest, "authorization request").url).searchParams.get("resource")).toBe(
|
|
515
|
+
harness.handle.mcpUrl
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const tokenRequest = harness.requests.find((request) => {
|
|
519
|
+
if (
|
|
520
|
+
request.method !== "POST"
|
|
521
|
+
|| request.url !== `${harness.handle.oauth.issuer}/token`
|
|
522
|
+
) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return parseFormBody(request).get("grant_type") === "authorization_code";
|
|
527
|
+
});
|
|
528
|
+
expect(parseFormBody(requireRequest(tokenRequest, "authorization-code token request")).get("resource")).toBe(
|
|
529
|
+
harness.handle.mcpUrl
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const secondCallBaseline = summarizeTraffic(harness.requests, harness.handle);
|
|
533
|
+
const secondResult = await harness.client.callTool({
|
|
534
|
+
name: "echo",
|
|
535
|
+
arguments: {
|
|
536
|
+
text: "second-call",
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
expect(getTextContent(secondResult)).toBe("second-call");
|
|
541
|
+
|
|
542
|
+
const summaryAfterSecondCall = summarizeTraffic(harness.requests, harness.handle);
|
|
543
|
+
const authorizationServerSummaryAfterSecondCall = summarizeAuthorizationServerTraffic(
|
|
544
|
+
harness.handle
|
|
545
|
+
);
|
|
546
|
+
expect(summaryAfterSecondCall.authorize - secondCallBaseline.authorize).toBe(0);
|
|
547
|
+
expect(summaryAfterSecondCall.tokenAuthorizationCode - secondCallBaseline.tokenAuthorizationCode).toBe(0);
|
|
548
|
+
expect(summaryAfterSecondCall.tokenRefresh - secondCallBaseline.tokenRefresh).toBe(0);
|
|
549
|
+
expect(summaryAfterSecondCall.mcpPost - secondCallBaseline.mcpPost).toBe(1);
|
|
550
|
+
expect(authorizationServerSummaryAfterSecondCall).toEqual({
|
|
551
|
+
authorize: 1,
|
|
552
|
+
metadata: 1,
|
|
553
|
+
register: 1,
|
|
554
|
+
tokenAuthorizationCode: 1,
|
|
555
|
+
tokenRefresh: 0,
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("skips DCR for a configured static client and still completes the PKCE flow", async () => {
|
|
560
|
+
const redirectPort = await reservePort("127.0.0.1");
|
|
561
|
+
const redirectUri = `http://127.0.0.1:${redirectPort}/callback`;
|
|
562
|
+
const harness = await createHarness({
|
|
563
|
+
createServer: createFixedPortServerFactory(redirectPort),
|
|
564
|
+
oauthClient: {
|
|
565
|
+
mode: "static",
|
|
566
|
+
clientId: "static-client",
|
|
567
|
+
},
|
|
568
|
+
serverOptions: {
|
|
569
|
+
staticClients: [
|
|
570
|
+
{
|
|
571
|
+
clientId: "static-client",
|
|
572
|
+
redirectUris: [redirectUri],
|
|
573
|
+
scopes: ["mcp.read"],
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
cleanups.add(harness.close);
|
|
579
|
+
|
|
580
|
+
await harness.client.connect(harness.transport);
|
|
581
|
+
|
|
582
|
+
const result = await harness.client.callTool({
|
|
583
|
+
name: "echo",
|
|
584
|
+
arguments: {
|
|
585
|
+
text: "static-client",
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
expect(getTextContent(result)).toBe("static-client");
|
|
590
|
+
|
|
591
|
+
const summary = summarizeTraffic(harness.requests, harness.handle);
|
|
592
|
+
const authorizationServerSummary = summarizeAuthorizationServerTraffic(
|
|
593
|
+
harness.handle
|
|
594
|
+
);
|
|
595
|
+
expect(summary.register).toBe(0);
|
|
596
|
+
expect(summary.authorize).toBe(1);
|
|
597
|
+
expect(summary.tokenAuthorizationCode).toBe(1);
|
|
598
|
+
expect(authorizationServerSummary).toEqual({
|
|
599
|
+
authorize: 1,
|
|
600
|
+
metadata: 1,
|
|
601
|
+
register: 0,
|
|
602
|
+
tokenAuthorizationCode: 1,
|
|
603
|
+
tokenRefresh: 0,
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const authorizationRequest = harness.requests.find(
|
|
607
|
+
(request) =>
|
|
608
|
+
request.method === "GET"
|
|
609
|
+
&& request.url.startsWith(`${harness.handle.oauth.issuer}/authorize`)
|
|
610
|
+
);
|
|
611
|
+
expect(new URL(requireRequest(authorizationRequest, "authorization request").url).searchParams.get("client_id")).toBe(
|
|
612
|
+
"static-client"
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const tokenRequest = harness.requests.find((request) => {
|
|
616
|
+
if (
|
|
617
|
+
request.method !== "POST"
|
|
618
|
+
|| request.url !== `${harness.handle.oauth.issuer}/token`
|
|
619
|
+
) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return parseFormBody(request).get("grant_type") === "authorization_code";
|
|
624
|
+
});
|
|
625
|
+
expect(parseFormBody(requireRequest(tokenRequest, "authorization-code token request")).get("client_id")).toBe(
|
|
626
|
+
"static-client"
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("refreshes once after the current access token is revoked and the MCP server returns invalid_token", async () => {
|
|
631
|
+
const harness = await createHarness();
|
|
632
|
+
cleanups.add(harness.close);
|
|
633
|
+
|
|
634
|
+
await harness.client.connect(harness.transport);
|
|
635
|
+
await harness.client.callTool({
|
|
636
|
+
name: "echo",
|
|
637
|
+
arguments: {
|
|
638
|
+
text: "before-revoke",
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const initialSession = getStoredSession(harness);
|
|
643
|
+
expect(initialSession.tokens?.accessToken).toBeTruthy();
|
|
644
|
+
const revokedAccessToken = initialSession.tokens?.accessToken ?? "";
|
|
645
|
+
harness.handle.oauth.revoke(revokedAccessToken);
|
|
646
|
+
|
|
647
|
+
const baseline = summarizeTraffic(harness.requests, harness.handle);
|
|
648
|
+
const authorizationServerBaseline = summarizeAuthorizationServerTraffic(
|
|
649
|
+
harness.handle
|
|
650
|
+
);
|
|
651
|
+
const refreshedResult = await harness.client.callTool({
|
|
652
|
+
name: "echo",
|
|
653
|
+
arguments: {
|
|
654
|
+
text: "after-revoke",
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
expect(getTextContent(refreshedResult)).toBe("after-revoke");
|
|
659
|
+
|
|
660
|
+
const summary = summarizeTraffic(harness.requests, harness.handle);
|
|
661
|
+
const authorizationServerSummary = summarizeAuthorizationServerTraffic(
|
|
662
|
+
harness.handle
|
|
663
|
+
);
|
|
664
|
+
expect(summary.authorize - baseline.authorize).toBe(0);
|
|
665
|
+
expect(summary.tokenRefresh - baseline.tokenRefresh).toBe(1);
|
|
666
|
+
expect(summary.mcpPost - baseline.mcpPost).toBe(2);
|
|
667
|
+
expect(
|
|
668
|
+
authorizationServerSummary.tokenRefresh - authorizationServerBaseline.tokenRefresh
|
|
669
|
+
).toBe(1);
|
|
670
|
+
|
|
671
|
+
const refreshRequest = findLastRequest(harness.requests, (request) => {
|
|
672
|
+
if (
|
|
673
|
+
request.method !== "POST"
|
|
674
|
+
|| request.url !== `${harness.handle.oauth.issuer}/token`
|
|
675
|
+
) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return parseFormBody(request).get("grant_type") === "refresh_token";
|
|
680
|
+
}, "refresh token request");
|
|
681
|
+
expect(parseFormBody(refreshRequest).get("resource")).toBe(
|
|
682
|
+
harness.handle.mcpUrl
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
const updatedSession = getStoredSession(harness);
|
|
686
|
+
expect(updatedSession.tokens?.accessToken).toBeTruthy();
|
|
687
|
+
expect(updatedSession.tokens?.accessToken).not.toBe(revokedAccessToken);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("deduplicates concurrent refreshes when multiple calls race on an expired token", async () => {
|
|
691
|
+
const harness = await createHarness();
|
|
692
|
+
cleanups.add(harness.close);
|
|
693
|
+
|
|
694
|
+
await harness.client.connect(harness.transport);
|
|
695
|
+
await harness.client.callTool({
|
|
696
|
+
name: "echo",
|
|
697
|
+
arguments: {
|
|
698
|
+
text: "seed-token",
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
harness.setNow(80_000);
|
|
703
|
+
const baseline = summarizeTraffic(harness.requests, harness.handle);
|
|
704
|
+
const authorizationServerBaseline = summarizeAuthorizationServerTraffic(
|
|
705
|
+
harness.handle
|
|
706
|
+
);
|
|
707
|
+
const results = await Promise.all(
|
|
708
|
+
Array.from({ length: 5 }, (_, index) =>
|
|
709
|
+
harness.client.callTool({
|
|
710
|
+
name: "echo",
|
|
711
|
+
arguments: {
|
|
712
|
+
text: `parallel-${index}`,
|
|
713
|
+
},
|
|
714
|
+
})
|
|
715
|
+
)
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
expect(results.map(getTextContent)).toEqual([
|
|
719
|
+
"parallel-0",
|
|
720
|
+
"parallel-1",
|
|
721
|
+
"parallel-2",
|
|
722
|
+
"parallel-3",
|
|
723
|
+
"parallel-4",
|
|
724
|
+
]);
|
|
725
|
+
|
|
726
|
+
const summary = summarizeTraffic(harness.requests, harness.handle);
|
|
727
|
+
const authorizationServerSummary = summarizeAuthorizationServerTraffic(
|
|
728
|
+
harness.handle
|
|
729
|
+
);
|
|
730
|
+
expect(summary.authorize - baseline.authorize).toBe(0);
|
|
731
|
+
expect(summary.tokenRefresh - baseline.tokenRefresh).toBe(1);
|
|
732
|
+
expect(summary.mcpPost - baseline.mcpPost).toBe(5);
|
|
733
|
+
expect(
|
|
734
|
+
authorizationServerSummary.tokenRefresh - authorizationServerBaseline.tokenRefresh
|
|
735
|
+
).toBe(1);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("maps verifier audience mismatches to a typed OAuthError instead of a generic transport error", async () => {
|
|
739
|
+
const harness = await createHarness({
|
|
740
|
+
responseTransform: async ({ handle, record, response }) => {
|
|
741
|
+
if (
|
|
742
|
+
record.method !== "POST"
|
|
743
|
+
|| record.url !== `${handle.oauth.issuer}/token`
|
|
744
|
+
|| parseFormBody(record).get("grant_type") !== "authorization_code"
|
|
745
|
+
) {
|
|
746
|
+
return undefined;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const payload = await response.clone().json() as Record<string, unknown>;
|
|
750
|
+
const wrongAudienceToken = await handle.oauth.issueTokenFor({
|
|
751
|
+
clientId: parseFormBody(record).get("client_id") ?? "unknown-client",
|
|
752
|
+
resource: `${handle.mcpUrl}/wrong-audience`,
|
|
753
|
+
scopes: ["mcp.read"],
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
payload.access_token = wrongAudienceToken;
|
|
757
|
+
delete payload.refresh_token;
|
|
758
|
+
|
|
759
|
+
return cloneResponse(response, JSON.stringify(payload));
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
cleanups.add(harness.close);
|
|
763
|
+
|
|
764
|
+
let caughtError: unknown;
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
await harness.client.connect(harness.transport);
|
|
768
|
+
} catch (error) {
|
|
769
|
+
caughtError = error;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
expect(caughtError).toBeInstanceOf(OAuthError);
|
|
773
|
+
expect(caughtError).toMatchObject({
|
|
774
|
+
error: "invalid_token",
|
|
775
|
+
errorDescription: "audience mismatch",
|
|
776
|
+
status: 401,
|
|
777
|
+
});
|
|
778
|
+
expect((caughtError as Error).message).toBe("audience mismatch");
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("maps insufficient_scope bearer challenges to a typed OAuthError", async () => {
|
|
782
|
+
const harness = await createHarness({
|
|
783
|
+
responseTransform: async ({ handle, record, response }) => {
|
|
784
|
+
if (
|
|
785
|
+
record.method !== "POST"
|
|
786
|
+
|| record.url !== `${handle.oauth.issuer}/token`
|
|
787
|
+
|| parseFormBody(record).get("grant_type") !== "authorization_code"
|
|
788
|
+
) {
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const payload = await response.clone().json() as Record<string, unknown>;
|
|
793
|
+
const insufficientScopeToken = await handle.oauth.issueTokenFor({
|
|
794
|
+
clientId: parseFormBody(record).get("client_id") ?? "unknown-client",
|
|
795
|
+
resource: handle.mcpUrl,
|
|
796
|
+
scopes: ["mcp.write"],
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
payload.access_token = insufficientScopeToken;
|
|
800
|
+
delete payload.refresh_token;
|
|
801
|
+
|
|
802
|
+
return cloneResponse(response, JSON.stringify(payload));
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
cleanups.add(harness.close);
|
|
806
|
+
|
|
807
|
+
let caughtError: unknown;
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
await harness.client.connect(harness.transport);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
caughtError = error;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
expect(caughtError).toBeInstanceOf(OAuthError);
|
|
816
|
+
expect(caughtError).toMatchObject({
|
|
817
|
+
error: "insufficient_scope",
|
|
818
|
+
errorDescription: "insufficient scope",
|
|
819
|
+
status: 403,
|
|
820
|
+
});
|
|
821
|
+
expect((caughtError as Error).message).toBe("insufficient scope");
|
|
822
|
+
});
|
|
823
|
+
});
|