tiny-http-mcp-server 0.1.17 → 0.1.19
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/cli.js +14 -2
- package/dist/composition.json +1 -1
- package/dist/http-server.d.ts +7 -5
- package/dist/http-server.js +3 -11
- package/dist/http-transport.d.ts +6 -1
- package/dist/http-transport.js +196 -15
- package/dist/index.d.ts +2 -2
- package/dist/modern-headers.d.ts +3 -0
- package/dist/modern-headers.js +35 -0
- package/dist/parse-body.d.ts +1 -0
- package/dist/parse-body.js +9 -5
- package/dist/test-support.js +17 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +3 -2
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +21 -4
- package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +1 -1
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +14 -6
- package/node_modules/mcp-oauth/dist/http-fetch.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/http-fetch.js +8 -0
- package/node_modules/mcp-oauth/dist/http-response.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/http-response.js +50 -0
- package/node_modules/mcp-oauth/dist/index.d.ts +2 -0
- package/node_modules/mcp-oauth/dist/index.js +2 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +21 -3
- package/node_modules/tiny-mcp-client/README.md +1 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +178 -101
- package/node_modules/tiny-mcp-client/dist/index.js +4696 -291
- package/node_modules/tiny-mcp-client/package.json +2 -1
- package/package.json +1 -1
package/dist/test-support.js
CHANGED
|
@@ -166,6 +166,7 @@ async function fetchInMemory(server, url, init) {
|
|
|
166
166
|
response.headerValues = new Headers();
|
|
167
167
|
response.headersSent = false;
|
|
168
168
|
response.writableEnded = false;
|
|
169
|
+
response.destroyed = false;
|
|
169
170
|
const setResponseHeader = (name, value) => {
|
|
170
171
|
response.headerValues.delete(name);
|
|
171
172
|
if (Array.isArray(value) && name.toLowerCase() === "set-cookie") {
|
|
@@ -224,6 +225,22 @@ async function fetchInMemory(server, url, init) {
|
|
|
224
225
|
}
|
|
225
226
|
return response;
|
|
226
227
|
});
|
|
228
|
+
response.destroy = ((error) => {
|
|
229
|
+
if (!response.destroyed) {
|
|
230
|
+
response.destroyed = true;
|
|
231
|
+
try {
|
|
232
|
+
if (error === undefined)
|
|
233
|
+
streamController?.close();
|
|
234
|
+
else
|
|
235
|
+
streamController?.error(error);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// The consumer may have cancelled the stream already.
|
|
239
|
+
}
|
|
240
|
+
response.emit("close");
|
|
241
|
+
}
|
|
242
|
+
return response;
|
|
243
|
+
});
|
|
227
244
|
response.flushHeaders = (() => {
|
|
228
245
|
response.headersSent = true;
|
|
229
246
|
});
|
package/dist/testing.d.ts
CHANGED
|
@@ -56,4 +56,4 @@ export declare function createInMemoryTokenVerifier(options?: Partial<{
|
|
|
56
56
|
now: () => number;
|
|
57
57
|
}>): InMemoryTokenVerifier;
|
|
58
58
|
export declare function createHttpTestPair(server: HttpServer): Promise<HttpTestPair>;
|
|
59
|
-
export declare function createHttpTestPairWithTinyClient(server: HttpServer): Promise<TinyHttpTestPair>;
|
|
59
|
+
export declare function createHttpTestPairWithTinyClient(server: HttpServer, clientOptions?: Pick<import("tiny-mcp-client").McpClientOptions, "protocolVersion">): Promise<TinyHttpTestPair>;
|
package/dist/testing.js
CHANGED
|
@@ -135,7 +135,7 @@ export async function createHttpTestPair(server) {
|
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
|
-
export async function createHttpTestPairWithTinyClient(server) {
|
|
138
|
+
export async function createHttpTestPairWithTinyClient(server, clientOptions = {}) {
|
|
139
139
|
let tinyMcpClient;
|
|
140
140
|
try {
|
|
141
141
|
tinyMcpClient = await import("tiny-mcp-client");
|
|
@@ -147,7 +147,8 @@ export async function createHttpTestPairWithTinyClient(server) {
|
|
|
147
147
|
const handle = await server.listenHttp({ port: 0 });
|
|
148
148
|
const requests = [];
|
|
149
149
|
const client = new tinyMcpClient.McpClient({
|
|
150
|
-
clientInfo: { name: "tiny-http-test-client", version: "1.0.0" }
|
|
150
|
+
clientInfo: { name: "tiny-http-test-client", version: "1.0.0" },
|
|
151
|
+
...clientOptions
|
|
151
152
|
});
|
|
152
153
|
const transport = new tinyMcpClient.HttpTransport({
|
|
153
154
|
url: handle.url,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { fetchMcpResponse } from "../http-fetch.js";
|
|
1
3
|
import { URL } from "node:url";
|
|
2
4
|
import { createAuthStoreClientStore, createAuthStoreSessionStore } from "./auth-store-session-store.js";
|
|
3
5
|
import { createLoopbackAuthorizationSession } from "./loopback-authorization.js";
|
|
@@ -62,6 +64,15 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
62
64
|
async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false) {
|
|
63
65
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
64
66
|
let session = await loadSession(canonicalResource);
|
|
67
|
+
if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
68
|
+
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
69
|
+
}
|
|
70
|
+
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
|
|
71
|
+
|| getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
|
|
72
|
+
|| (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
|
|
73
|
+
await clearSession(canonicalResource);
|
|
74
|
+
session = null;
|
|
75
|
+
}
|
|
65
76
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
66
77
|
if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
67
78
|
return session;
|
|
@@ -281,14 +292,16 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
281
292
|
}
|
|
282
293
|
}
|
|
283
294
|
const registrationBody = buildClientRegistrationBody(getClientMetadata(options.client), redirectUri);
|
|
284
|
-
const
|
|
295
|
+
const signal = AbortSignal.timeout(30_000);
|
|
296
|
+
const response = await fetchMcpResponse(fetch, registrationEndpoint, {
|
|
285
297
|
method: "POST",
|
|
286
298
|
headers: {
|
|
287
299
|
"Content-Type": "application/json"
|
|
288
300
|
},
|
|
289
|
-
body: JSON.stringify(registrationBody)
|
|
301
|
+
body: JSON.stringify(registrationBody),
|
|
302
|
+
signal
|
|
290
303
|
});
|
|
291
|
-
const payload = await readOAuthJsonObjectResponse(response);
|
|
304
|
+
const payload = await readOAuthJsonObjectResponse(response, signal);
|
|
292
305
|
const clientId = getOwnString(payload, "client_id");
|
|
293
306
|
if (clientId === undefined || clientId.trim().length === 0) {
|
|
294
307
|
throw new Error("OAuth client registration response missing client_id");
|
|
@@ -545,10 +558,14 @@ function isLoopbackHostname(hostname) {
|
|
|
545
558
|
const normalizedHostname = normalizeHostname(hostname);
|
|
546
559
|
return (normalizedHostname === "localhost" ||
|
|
547
560
|
normalizedHostname === "::1" ||
|
|
548
|
-
normalizedHostname
|
|
561
|
+
normalizedHostname === "[::1]" ||
|
|
562
|
+
(isIP(normalizedHostname) === 4 && normalizedHostname.startsWith("127.")));
|
|
549
563
|
}
|
|
550
564
|
function assertSecureUrl(value, label) {
|
|
551
565
|
const url = new URL(value);
|
|
566
|
+
if (url.username !== "" || url.password !== "" || url.hash !== "") {
|
|
567
|
+
throw new Error(`${label} must not include credentials or fragment`);
|
|
568
|
+
}
|
|
552
569
|
if (url.protocol === "https:") {
|
|
553
570
|
return;
|
|
554
571
|
}
|
|
@@ -36,5 +36,5 @@ export declare function refreshAccessToken(input: {
|
|
|
36
36
|
fetch: OAuthMetadataFetch;
|
|
37
37
|
now: () => number;
|
|
38
38
|
}): Promise<StoredOAuthTokens>;
|
|
39
|
-
export declare function readOAuthJsonObjectResponse(response: Response): Promise<Record<string, unknown>>;
|
|
39
|
+
export declare function readOAuthJsonObjectResponse(response: Response, signal?: AbortSignal): Promise<Record<string, unknown>>;
|
|
40
40
|
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { canonicalizeResourceIndicator } from "../resource-indicator.js";
|
|
2
|
+
import { readBoundedResponseText } from "../http-response.js";
|
|
3
|
+
import { fetchMcpResponse } from "../http-fetch.js";
|
|
2
4
|
const MAX_JS_DATE_MS = 8_640_000_000_000_000;
|
|
3
5
|
export class OAuthError extends Error {
|
|
4
6
|
error;
|
|
@@ -68,14 +70,16 @@ async function requestTokens(input) {
|
|
|
68
70
|
if (input.clientSecret !== undefined) {
|
|
69
71
|
body.set("client_secret", input.clientSecret);
|
|
70
72
|
}
|
|
71
|
-
const
|
|
73
|
+
const signal = AbortSignal.timeout(30_000);
|
|
74
|
+
const response = await fetchMcpResponse(input.fetch, input.tokenEndpoint, {
|
|
72
75
|
method: "POST",
|
|
73
76
|
headers: {
|
|
74
77
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
75
78
|
},
|
|
76
|
-
body: body.toString()
|
|
79
|
+
body: body.toString(),
|
|
80
|
+
signal
|
|
77
81
|
});
|
|
78
|
-
const payload = await readOAuthJsonObjectResponse(response);
|
|
82
|
+
const payload = await readOAuthJsonObjectResponse(response, signal);
|
|
79
83
|
const accessToken = getOwnEntry(payload, "access_token");
|
|
80
84
|
if (typeof accessToken !== "string" || accessToken.trim().length === 0) {
|
|
81
85
|
throw new Error("OAuth token response missing access_token");
|
|
@@ -115,13 +119,17 @@ async function requestTokens(input) {
|
|
|
115
119
|
scope: normalizedScope === undefined ? undefined : normalizedScope
|
|
116
120
|
};
|
|
117
121
|
}
|
|
118
|
-
export async function readOAuthJsonObjectResponse(response) {
|
|
122
|
+
export async function readOAuthJsonObjectResponse(response, signal) {
|
|
119
123
|
const fallbackError = createFallbackOAuthError(response.status);
|
|
120
124
|
let payload;
|
|
121
125
|
try {
|
|
122
|
-
payload = await response
|
|
126
|
+
payload = JSON.parse(await readBoundedResponseText(response, 1024 * 1024, undefined, signal));
|
|
123
127
|
}
|
|
124
|
-
catch {
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (signal?.aborted)
|
|
130
|
+
throw signal.reason;
|
|
131
|
+
if (error instanceof Error && error.message.startsWith("HTTP response exceeds "))
|
|
132
|
+
throw error;
|
|
125
133
|
if (!response.ok) {
|
|
126
134
|
throw fallbackError;
|
|
127
135
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fetchMcpResponse(fetchImplementation: (input: string | URL, init?: RequestInit) => Promise<Response>, input: string | URL, init?: RequestInit): Promise<Response>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export async function fetchMcpResponse(fetchImplementation, input, init = {}) {
|
|
2
|
+
const response = await fetchImplementation(input, { ...init, redirect: "error" });
|
|
3
|
+
if (response.redirected || response.type === "opaqueredirect") {
|
|
4
|
+
void response.body?.cancel().catch(() => undefined);
|
|
5
|
+
throw new Error("MCP HTTP redirects are not allowed");
|
|
6
|
+
}
|
|
7
|
+
return response;
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readBoundedResponseText(response: Response, maxBytes: number, readers?: Set<ReadableStreamDefaultReader<Uint8Array>>, signal?: AbortSignal): Promise<string>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export async function readBoundedResponseText(response, maxBytes, readers, signal) {
|
|
2
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
|
|
3
|
+
throw new Error("HTTP response byte limit must be a positive safe integer");
|
|
4
|
+
if (signal?.aborted) {
|
|
5
|
+
void response.body?.cancel().catch(() => undefined);
|
|
6
|
+
throw signal.reason;
|
|
7
|
+
}
|
|
8
|
+
const length = response.headers.get("Content-Length");
|
|
9
|
+
if (length !== null &&
|
|
10
|
+
length.length > 0 &&
|
|
11
|
+
[...length].every((character) => character >= "0" && character <= "9") &&
|
|
12
|
+
Number(length) > maxBytes) {
|
|
13
|
+
void response.body?.cancel().catch(() => undefined);
|
|
14
|
+
throw new Error(`HTTP response exceeds ${maxBytes} bytes`);
|
|
15
|
+
}
|
|
16
|
+
if (response.body === null)
|
|
17
|
+
return "";
|
|
18
|
+
const reader = response.body.getReader();
|
|
19
|
+
readers?.add(reader);
|
|
20
|
+
const abort = () => {
|
|
21
|
+
void reader.cancel().catch(() => undefined);
|
|
22
|
+
};
|
|
23
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
24
|
+
if (signal?.aborted)
|
|
25
|
+
abort();
|
|
26
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
let text = "";
|
|
29
|
+
try {
|
|
30
|
+
while (true) {
|
|
31
|
+
const chunk = await reader.read();
|
|
32
|
+
signal?.throwIfAborted();
|
|
33
|
+
if (chunk.done)
|
|
34
|
+
return text + decoder.decode();
|
|
35
|
+
bytes += chunk.value.byteLength;
|
|
36
|
+
if (bytes > maxBytes)
|
|
37
|
+
throw new Error(`HTTP response exceeds ${maxBytes} bytes`);
|
|
38
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
void reader.cancel().catch(() => undefined);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
signal?.removeEventListener("abort", abort);
|
|
47
|
+
readers?.delete(reader);
|
|
48
|
+
reader.releaseLock();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -8,3 +8,5 @@ export { createJwksTokenVerifier, } from "./server/jwks-token-verifier.js";
|
|
|
8
8
|
export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, StoredOAuthSession, StoredOAuthTokens, } from "./client/types.js";
|
|
9
9
|
export type { JwksTokenVerifier, JwksTokenVerifierOptions, JwksVerifiedAccessToken, } from "./server/jwks-token-verifier.js";
|
|
10
10
|
export type { LoopbackAuthorizationOptions, LoopbackAuthorizationSession, OAuthLandingPage, } from "./client/loopback-authorization.js";
|
|
11
|
+
export { readBoundedResponseText } from "./http-response.js";
|
|
12
|
+
export { fetchMcpResponse } from "./http-fetch.js";
|
|
@@ -5,3 +5,5 @@ export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
|
|
|
5
5
|
export { OAuthError, } from "./client/token-endpoint.js";
|
|
6
6
|
export { canonicalizeResourceIndicator, } from "./resource-indicator.js";
|
|
7
7
|
export { createJwksTokenVerifier, } from "./server/jwks-token-verifier.js";
|
|
8
|
+
export { readBoundedResponseText } from "./http-response.js";
|
|
9
|
+
export { fetchMcpResponse } from "./http-fetch.js";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readBoundedResponseText } from "../http-response.js";
|
|
2
|
+
import { fetchMcpResponse } from "../http-fetch.js";
|
|
1
3
|
import { decodeProtectedHeader, errors, importJWK, jwtVerify } from "jose";
|
|
2
4
|
import { canonicalizeResourceIndicator } from "../resource-indicator.js";
|
|
3
5
|
const DEFAULT_ALLOWED_ALGORITHMS = [
|
|
@@ -158,16 +160,18 @@ function normalizeVerificationError(error) {
|
|
|
158
160
|
}
|
|
159
161
|
async function loadJwks(jwksUrl, fetchImplementation, timeoutMs) {
|
|
160
162
|
try {
|
|
161
|
-
const
|
|
163
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
164
|
+
const response = await fetchMcpResponse(fetchImplementation, jwksUrl, {
|
|
162
165
|
headers: {
|
|
163
166
|
Accept: "application/json"
|
|
164
167
|
},
|
|
165
|
-
signal
|
|
168
|
+
signal
|
|
166
169
|
});
|
|
167
170
|
if (!response.ok) {
|
|
171
|
+
await response.body?.cancel().catch(() => undefined);
|
|
168
172
|
throw createTemporarilyUnavailableError();
|
|
169
173
|
}
|
|
170
|
-
const payload = (await response
|
|
174
|
+
const payload = JSON.parse(await readBoundedResponseText(response, 1024 * 1024, undefined, signal));
|
|
171
175
|
const keys = isObjectRecord(payload) ? getOwnEntry(payload, "keys") : undefined;
|
|
172
176
|
if (!isObjectRecord(payload) || !Array.isArray(keys) || !keys.every(isObjectRecord)) {
|
|
173
177
|
throw createTemporarilyUnavailableError();
|
|
@@ -280,6 +284,9 @@ function isLoopbackHostname(hostname) {
|
|
|
280
284
|
}
|
|
281
285
|
export function createJwksTokenVerifier(options) {
|
|
282
286
|
const jwksUrl = toUrl(options.jwksUrl, "jwksUrl");
|
|
287
|
+
if ((jwksUrl.protocol !== "http:" && jwksUrl.protocol !== "https:") || jwksUrl.username !== "" || jwksUrl.password !== "") {
|
|
288
|
+
throw new Error("jwksUrl must be an HTTP or HTTPS URL without credentials");
|
|
289
|
+
}
|
|
283
290
|
const clockSkewSeconds = options.clockSkewSeconds ?? 30;
|
|
284
291
|
const allowedAlgorithms = options.allowedAlgorithms ?? DEFAULT_ALLOWED_ALGORITHMS;
|
|
285
292
|
const jwksCacheTtlMs = options.jwksCacheTtlMs ?? 300_000;
|
|
@@ -287,6 +294,17 @@ export function createJwksTokenVerifier(options) {
|
|
|
287
294
|
const jwksRefreshCooldownMs = options.jwksRefreshCooldownMs ?? 30_000;
|
|
288
295
|
const requireAccessTokenType = options.requireAccessTokenType ?? false;
|
|
289
296
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
297
|
+
if (!Number.isFinite(clockSkewSeconds) || clockSkewSeconds < 0) {
|
|
298
|
+
throw new Error("clockSkewSeconds must be a finite non-negative number");
|
|
299
|
+
}
|
|
300
|
+
for (const key of ["jwksCacheTtlMs", "jwksRefreshCooldownMs"]) {
|
|
301
|
+
const value = key === "jwksCacheTtlMs" ? jwksCacheTtlMs : jwksRefreshCooldownMs;
|
|
302
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
303
|
+
throw new Error(`${key} must be a non-negative safe integer`);
|
|
304
|
+
}
|
|
305
|
+
if (!Number.isInteger(jwksFetchTimeoutMs) || jwksFetchTimeoutMs <= 0 || jwksFetchTimeoutMs > 2_147_483_647) {
|
|
306
|
+
throw new Error("jwksFetchTimeoutMs must be a positive integer no greater than 2147483647");
|
|
307
|
+
}
|
|
290
308
|
let cachedJwks;
|
|
291
309
|
let pendingJwks;
|
|
292
310
|
let pendingForcedRefresh;
|
|
@@ -88,6 +88,7 @@ You can also call `discoverOAuthMetadata(resourceUrl, options)` directly, or ins
|
|
|
88
88
|
| `fetch` | `(input, init?) => Promise<Response>` | Custom fetch implementation. |
|
|
89
89
|
| `oauth` | `OAuthClientProviderOptions` | Enables OAuth authorization handling. |
|
|
90
90
|
| `oauthDiscoveryCache` | `OAuthDiscoveryCache` | Optional shared metadata cache. |
|
|
91
|
+
| `maxResponseBytes` | `number` | Maximum JSON/error body or retained SSE event bytes; defaults to 16 MiB and must be a positive safe integer. |
|
|
91
92
|
|
|
92
93
|
### `StdioTransportOptions`
|
|
93
94
|
|