poe-code 3.0.300 → 3.0.302
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/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/packages/tiny-http-mcp-oauth-test-server/dist/cli.js +34 -16
- package/packages/tiny-http-mcp-oauth-test-server/dist/index.js +12 -2
- package/packages/tiny-http-mcp-server/dist/auth.js +22 -2
- package/packages/tiny-http-mcp-server/dist/cli.js +38 -14
- package/packages/tiny-http-mcp-server/dist/http-transport.d.ts +1 -0
- package/packages/tiny-http-mcp-server/dist/http-transport.js +25 -8
package/package.json
CHANGED
|
@@ -42,6 +42,9 @@ function parsePort(value) {
|
|
|
42
42
|
if (value === undefined) {
|
|
43
43
|
return 0;
|
|
44
44
|
}
|
|
45
|
+
if (!isDecimalInteger(value)) {
|
|
46
|
+
throw new Error("--port must be an integer between 0 and 65535.");
|
|
47
|
+
}
|
|
45
48
|
const port = Number(value);
|
|
46
49
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
47
50
|
throw new Error("--port must be an integer between 0 and 65535.");
|
|
@@ -52,12 +55,18 @@ function parsePositiveInteger(value, flagName) {
|
|
|
52
55
|
if (value === undefined) {
|
|
53
56
|
return 60;
|
|
54
57
|
}
|
|
58
|
+
if (!isDecimalInteger(value)) {
|
|
59
|
+
throw new Error(`${flagName} must be a positive integer.`);
|
|
60
|
+
}
|
|
55
61
|
const parsed = Number(value);
|
|
56
62
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
57
63
|
throw new Error(`${flagName} must be a positive integer.`);
|
|
58
64
|
}
|
|
59
65
|
return parsed;
|
|
60
66
|
}
|
|
67
|
+
function isDecimalInteger(value) {
|
|
68
|
+
return value.length > 0 && [...value].every((character) => character >= "0" && character <= "9");
|
|
69
|
+
}
|
|
61
70
|
function parseAbsoluteUrl(value, flagName) {
|
|
62
71
|
if (value === undefined) {
|
|
63
72
|
return undefined;
|
|
@@ -88,8 +97,10 @@ function parseScopes(value) {
|
|
|
88
97
|
}
|
|
89
98
|
const scopes = value
|
|
90
99
|
.split(",")
|
|
91
|
-
.map((scope) => scope.trim())
|
|
92
|
-
|
|
100
|
+
.map((scope) => scope.trim());
|
|
101
|
+
if (scopes.some((scope) => scope.length === 0)) {
|
|
102
|
+
throw new Error("--scopes must not contain empty entries.");
|
|
103
|
+
}
|
|
93
104
|
if (scopes.length === 0) {
|
|
94
105
|
throw new Error("--scopes must include at least one non-empty scope.");
|
|
95
106
|
}
|
|
@@ -178,20 +189,27 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
|
|
|
178
189
|
port: parsed.port,
|
|
179
190
|
hostname: parsed.hostname,
|
|
180
191
|
};
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
let handle;
|
|
193
|
+
try {
|
|
194
|
+
const server = createMcpOAuthTestServer(serverOptions);
|
|
195
|
+
handle = await server.listen(listenOptions);
|
|
196
|
+
stdout.write(`${packageInfo.name} ${packageInfo.version}\n`);
|
|
197
|
+
stdout.write(`MCP URL: ${handle.mcpUrl}\n`);
|
|
198
|
+
stdout.write(`PRM URL: ${handle.prmUrl}\n`);
|
|
199
|
+
stdout.write(`AS issuer: ${handle.oauth.issuer}\n`);
|
|
200
|
+
stdout.write(`Resource: ${handle.resource}\n`);
|
|
201
|
+
if (parsed.printTestToken) {
|
|
202
|
+
const token = await handle.oauth.issueTokenFor({
|
|
203
|
+
clientId: "demo-client",
|
|
204
|
+
resource: handle.resource,
|
|
205
|
+
scopes: parsed.scopes ?? ["mcp.read"],
|
|
206
|
+
});
|
|
207
|
+
stdout.write(`Test bearer token: ${token}\n`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${HELP_TEXT}\n`);
|
|
212
|
+
return 1;
|
|
195
213
|
}
|
|
196
214
|
await (dependencies.waitForShutdown ?? waitForShutdown)(handle.close);
|
|
197
215
|
return 0;
|
|
@@ -85,6 +85,9 @@ function normalizeScopes(scopes) {
|
|
|
85
85
|
if (normalizedScopes.some((scope) => scope.trim().length === 0)) {
|
|
86
86
|
throw new Error("scopes must contain non-empty values");
|
|
87
87
|
}
|
|
88
|
+
if (normalizedScopes.some((scope) => scope.trim() !== scope || scope.includes(" "))) {
|
|
89
|
+
throw new Error("scope entries must not contain spaces");
|
|
90
|
+
}
|
|
88
91
|
return normalizedScopes;
|
|
89
92
|
}
|
|
90
93
|
function normalizeTtlSeconds(ttlSeconds) {
|
|
@@ -94,6 +97,13 @@ function normalizeTtlSeconds(ttlSeconds) {
|
|
|
94
97
|
}
|
|
95
98
|
return normalizedTtlSeconds;
|
|
96
99
|
}
|
|
100
|
+
function normalizeListenPort(port) {
|
|
101
|
+
const normalizedPort = port ?? 0;
|
|
102
|
+
if (!Number.isInteger(normalizedPort) || normalizedPort < 0 || normalizedPort > 65535) {
|
|
103
|
+
throw new Error("port must be an integer between 0 and 65535");
|
|
104
|
+
}
|
|
105
|
+
return normalizedPort;
|
|
106
|
+
}
|
|
97
107
|
function closeServer(server) {
|
|
98
108
|
return new Promise((resolve, reject) => {
|
|
99
109
|
server.close((error) => {
|
|
@@ -135,9 +145,9 @@ export function createMcpOAuthTestServer(options = {}) {
|
|
|
135
145
|
if (currentHandle !== null || listenPending) {
|
|
136
146
|
throw new Error("MCP OAuth test server is already listening");
|
|
137
147
|
}
|
|
138
|
-
listenPending = true;
|
|
139
148
|
const hostname = listenOptions.hostname ?? "127.0.0.1";
|
|
140
|
-
const requestedPort = listenOptions.port
|
|
149
|
+
const requestedPort = normalizeListenPort(listenOptions.port);
|
|
150
|
+
listenPending = true;
|
|
141
151
|
let lastError;
|
|
142
152
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
143
153
|
try {
|
|
@@ -66,6 +66,19 @@ function formatScope(scope) {
|
|
|
66
66
|
}
|
|
67
67
|
return scope.join(" ");
|
|
68
68
|
}
|
|
69
|
+
function hasBearerTokenWhitespace(value) {
|
|
70
|
+
for (const character of value) {
|
|
71
|
+
if (character === " "
|
|
72
|
+
|| character === "\t"
|
|
73
|
+
|| character === "\n"
|
|
74
|
+
|| character === "\r"
|
|
75
|
+
|| character === "\f"
|
|
76
|
+
|| character === "\v") {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
69
82
|
function readBearerToken(req) {
|
|
70
83
|
const authorization = readSingleHeaderValue(req.headers.authorization);
|
|
71
84
|
if (authorization === undefined || authorization.length === 0) {
|
|
@@ -80,7 +93,7 @@ function readBearerToken(req) {
|
|
|
80
93
|
}
|
|
81
94
|
const scheme = authorization.slice(0, separatorIndex);
|
|
82
95
|
const token = authorization.slice(separatorIndex + 1).trim();
|
|
83
|
-
if (scheme.toLowerCase() !== "bearer" || token.length === 0 || token
|
|
96
|
+
if (scheme.toLowerCase() !== "bearer" || token.length === 0 || hasBearerTokenWhitespace(token)) {
|
|
84
97
|
return {
|
|
85
98
|
kind: "malformed",
|
|
86
99
|
errorDescription: "malformed bearer token",
|
|
@@ -153,7 +166,14 @@ function toRequestAuthInfo(verifiedToken, resource) {
|
|
|
153
166
|
};
|
|
154
167
|
}
|
|
155
168
|
export function getProtectedResourceMetadataUrl(req, protectedResourcePath, trustedProxy = false) {
|
|
156
|
-
|
|
169
|
+
const path = `${PROTECTED_RESOURCE_METADATA_PATH}${normalizeProtectedResourcePath(protectedResourcePath)}`;
|
|
170
|
+
const protocol = getRequestProtocol(req, trustedProxy);
|
|
171
|
+
try {
|
|
172
|
+
return new URL(path, `${protocol}://${getRequestHost(req, trustedProxy)}`).toString();
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return new URL(path, `${protocol}://127.0.0.1`).toString();
|
|
176
|
+
}
|
|
157
177
|
}
|
|
158
178
|
export function createBearerChallenge(req, options = {}, protectedResourcePath, trustedProxy = false) {
|
|
159
179
|
const parts = [
|
|
@@ -73,7 +73,7 @@ function parsePort(value) {
|
|
|
73
73
|
if (value === undefined) {
|
|
74
74
|
return 3000;
|
|
75
75
|
}
|
|
76
|
-
const port =
|
|
76
|
+
const port = parseDecimalInteger(value, "--port");
|
|
77
77
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
78
78
|
throw new Error("--port must be an integer between 0 and 65535.");
|
|
79
79
|
}
|
|
@@ -95,11 +95,24 @@ function parseOrigin(value, flagName) {
|
|
|
95
95
|
throw new Error(`${flagName} must be an absolute URL.`);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
function parseDecimalInteger(value, flagName) {
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (trimmed.length === 0) {
|
|
101
|
+
throw new Error(`${flagName} must be an integer.`);
|
|
102
|
+
}
|
|
103
|
+
for (const character of trimmed) {
|
|
104
|
+
const codePoint = character.codePointAt(0);
|
|
105
|
+
if (codePoint === undefined || codePoint < 48 || codePoint > 57) {
|
|
106
|
+
throw new Error(`${flagName} must be an integer.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return Number(trimmed);
|
|
110
|
+
}
|
|
98
111
|
function parseOptionalInteger(value, flagName, minimum) {
|
|
99
112
|
if (value === undefined) {
|
|
100
113
|
return undefined;
|
|
101
114
|
}
|
|
102
|
-
const parsed =
|
|
115
|
+
const parsed = parseDecimalInteger(value, flagName);
|
|
103
116
|
if (!Number.isInteger(parsed) || parsed < minimum) {
|
|
104
117
|
throw new Error(`${flagName} must be an integer greater than or equal to ${minimum}.`);
|
|
105
118
|
}
|
|
@@ -116,6 +129,23 @@ function hasConfiguredOAuthFlag(values) {
|
|
|
116
129
|
values["oauth-verifier-export"],
|
|
117
130
|
].some((value) => value !== undefined);
|
|
118
131
|
}
|
|
132
|
+
function parseRepeatableStrings(value, flagName) {
|
|
133
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const normalized = [];
|
|
137
|
+
for (const item of value) {
|
|
138
|
+
if (typeof item !== "string") {
|
|
139
|
+
throw new Error(`${flagName} must be provided as a string.`);
|
|
140
|
+
}
|
|
141
|
+
const trimmed = item.trim();
|
|
142
|
+
if (trimmed.length === 0) {
|
|
143
|
+
throw new Error(`${flagName} must not be blank.`);
|
|
144
|
+
}
|
|
145
|
+
normalized.push(trimmed);
|
|
146
|
+
}
|
|
147
|
+
return normalized;
|
|
148
|
+
}
|
|
119
149
|
function parseCliOAuthOptions(values) {
|
|
120
150
|
const resource = values["oauth-resource"];
|
|
121
151
|
const authorizationServers = values["oauth-authorization-server"];
|
|
@@ -134,21 +164,15 @@ function parseCliOAuthOptions(values) {
|
|
|
134
164
|
if (typeof verifierModule !== "string" || verifierModule.length === 0) {
|
|
135
165
|
throw new Error("--oauth-verifier-module is required when --oauth-resource is set.");
|
|
136
166
|
}
|
|
137
|
-
const supportedScopes = values["oauth-supported-scope"];
|
|
138
|
-
const requiredScopes = values["oauth-required-scope"];
|
|
139
|
-
const bearerMethods = values["oauth-bearer-method"];
|
|
167
|
+
const supportedScopes = parseRepeatableStrings(values["oauth-supported-scope"], "--oauth-supported-scope");
|
|
168
|
+
const requiredScopes = parseRepeatableStrings(values["oauth-required-scope"], "--oauth-required-scope");
|
|
169
|
+
const bearerMethods = parseRepeatableStrings(values["oauth-bearer-method"], "--oauth-bearer-method");
|
|
140
170
|
return {
|
|
141
171
|
resource: parseAbsoluteUrl(resource, "--oauth-resource"),
|
|
142
172
|
authorizationServers: authorizationServers.map((value) => parseAbsoluteUrl(value, "--oauth-authorization-server")),
|
|
143
|
-
...(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
...(Array.isArray(supportedScopes) && supportedScopes.length > 0
|
|
147
|
-
? { scopesSupported: [...supportedScopes] }
|
|
148
|
-
: {}),
|
|
149
|
-
...(Array.isArray(bearerMethods) && bearerMethods.length > 0
|
|
150
|
-
? { bearerMethodsSupported: [...bearerMethods] }
|
|
151
|
-
: {}),
|
|
173
|
+
...(requiredScopes === undefined ? {} : { requiredScopes }),
|
|
174
|
+
...(supportedScopes === undefined ? {} : { scopesSupported: supportedScopes }),
|
|
175
|
+
...(bearerMethods === undefined ? {} : { bearerMethodsSupported: bearerMethods }),
|
|
152
176
|
verifierModule,
|
|
153
177
|
verifierExport: typeof verifierExport === "string" && verifierExport.length > 0
|
|
154
178
|
? verifierExport
|
|
@@ -8,6 +8,15 @@ const ALLOWED_METHODS = "POST, GET, DELETE, OPTIONS";
|
|
|
8
8
|
const MCP_SESSION_ID_HEADER = "Mcp-Session-Id";
|
|
9
9
|
const LOCAL_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
10
10
|
const DEFAULT_ALLOWED_HEADERS = "Accept, Authorization, Content-Type, Mcp-Session-Id, MCP-Protocol-Version";
|
|
11
|
+
function validateOptionalIntegerOption(name, value, minimum) {
|
|
12
|
+
if (value === undefined) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
16
|
+
throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
11
20
|
export class StreamableHttpTransport {
|
|
12
21
|
server;
|
|
13
22
|
runWithRequestContext;
|
|
@@ -44,13 +53,14 @@ export class StreamableHttpTransport {
|
|
|
44
53
|
this.enableJsonResponse = options.enableJsonResponse ?? false;
|
|
45
54
|
this.allowedOrigins = new Set(options.allowedOrigins ?? []);
|
|
46
55
|
this.allowedHosts = new Set((options.allowedHosts ?? LOCAL_HOSTS).map((host) => this.normalizeHost(host)));
|
|
47
|
-
this.maxRequestBytes = options.maxRequestBytes;
|
|
48
|
-
this.maxBatchSize = options.maxBatchSize;
|
|
49
|
-
this.maxSessions = options.maxSessions;
|
|
50
|
-
this.sessionTtlMs = options.sessionTtlMs;
|
|
51
|
-
this.maxStreamsPerSession = options.maxStreamsPerSession;
|
|
52
|
-
this.maxSseEventHistory =
|
|
53
|
-
|
|
56
|
+
this.maxRequestBytes = validateOptionalIntegerOption("maxRequestBytes", options.maxRequestBytes, 1);
|
|
57
|
+
this.maxBatchSize = validateOptionalIntegerOption("maxBatchSize", options.maxBatchSize, 1);
|
|
58
|
+
this.maxSessions = validateOptionalIntegerOption("maxSessions", options.maxSessions, 1);
|
|
59
|
+
this.sessionTtlMs = validateOptionalIntegerOption("sessionTtlMs", options.sessionTtlMs, 1);
|
|
60
|
+
this.maxStreamsPerSession = validateOptionalIntegerOption("maxStreamsPerSession", options.maxStreamsPerSession, 1);
|
|
61
|
+
this.maxSseEventHistory =
|
|
62
|
+
validateOptionalIntegerOption("maxSseEventHistory", options.maxSseEventHistory, 0) ?? 100;
|
|
63
|
+
this.maxConcurrentToolCalls = validateOptionalIntegerOption("maxConcurrentToolCalls", options.maxConcurrentToolCalls, 1);
|
|
54
64
|
this.sessionStore = options.sessionStore ?? createSessionStore();
|
|
55
65
|
this.requestIdGenerator =
|
|
56
66
|
options.requestIdGenerator ?? (() => `req-${this.nextRequestId++}`);
|
|
@@ -328,6 +338,10 @@ export class StreamableHttpTransport {
|
|
|
328
338
|
});
|
|
329
339
|
return;
|
|
330
340
|
}
|
|
341
|
+
if (!this.acceptsResponseType(req, "text/event-stream")) {
|
|
342
|
+
this.respondWithStatus(res, 406);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
331
345
|
const sessionId = this.readSessionId(req);
|
|
332
346
|
if (sessionId === undefined) {
|
|
333
347
|
this.respondWithStatus(res, 400);
|
|
@@ -673,11 +687,14 @@ export class StreamableHttpTransport {
|
|
|
673
687
|
: normalized;
|
|
674
688
|
}
|
|
675
689
|
acceptsConfiguredResponse(req) {
|
|
690
|
+
const expectedType = this.enableJsonResponse ? "application/json" : "text/event-stream";
|
|
691
|
+
return this.acceptsResponseType(req, expectedType);
|
|
692
|
+
}
|
|
693
|
+
acceptsResponseType(req, expectedType) {
|
|
676
694
|
const accept = req.headers.accept;
|
|
677
695
|
if (accept === undefined) {
|
|
678
696
|
return true;
|
|
679
697
|
}
|
|
680
|
-
const expectedType = this.enableJsonResponse ? "application/json" : "text/event-stream";
|
|
681
698
|
const value = Array.isArray(accept) ? accept.join(",") : accept;
|
|
682
699
|
return value
|
|
683
700
|
.split(",")
|