toolcraft 0.0.142 → 0.0.144

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/composition.json CHANGED
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.5",
101
+ "version": "0.1.6",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.142",
116
+ "version": "0.0.144",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.142",
126
+ "version": "0.0.144",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.5",
101
+ "version": "0.1.6",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.142",
116
+ "version": "0.0.144",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.142",
126
+ "version": "0.0.144",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -14,7 +14,17 @@ const ignoredHostedOAuth = hostedOAuth({
14
14
  credential: `${ignoredPassword}:${ignoredSignal.aborted}`
15
15
  };
16
16
  },
17
- services({ credentials }) {
17
+ services({ credentials, identity }) {
18
+ const ignoredSubject = identity.subject;
19
+ const ignoredClientId = identity.clientId;
20
+ const ignoredScopes = identity.scopes;
21
+ const ignoredResource = identity.resource;
22
+ const ignoredIssuer = identity.issuer;
23
+ void ignoredSubject;
24
+ void ignoredClientId;
25
+ void ignoredScopes;
26
+ void ignoredResource;
27
+ void ignoredIssuer;
18
28
  return { skylight: { credential: () => credentials.read() } };
19
29
  }
20
30
  }
@@ -31,6 +31,7 @@ export interface HostedOAuthStorage<TCredential = unknown> {
31
31
  capabilities: HostedOAuthStorageCapabilities;
32
32
  signingKey(): Promise<OAuthAuthorizationServerSigningKey>;
33
33
  resolveSubject(providerName: string, accountId: string): Promise<string>;
34
+ healthCheck?(): Promise<void>;
34
35
  cleanup?(now?: number): Promise<void>;
35
36
  }
36
37
  export interface HostedOAuthCredentialAccess<TCredential = unknown> {
@@ -38,6 +39,13 @@ export interface HostedOAuthCredentialAccess<TCredential = unknown> {
38
39
  update(update: (credential: TCredential) => Promise<TCredential> | TCredential): Promise<TCredential>;
39
40
  delete(): Promise<void>;
40
41
  }
42
+ export interface HostedOAuthIdentity {
43
+ issuer: string;
44
+ subject: string;
45
+ clientId: string;
46
+ scopes: readonly string[];
47
+ resource: string;
48
+ }
41
49
  export interface HostedOAuthProvider<TCredential = unknown, TServices extends object = object> {
42
50
  name: string;
43
51
  login?: {
@@ -51,6 +59,7 @@ export interface HostedOAuthProvider<TCredential = unknown, TServices extends ob
51
59
  }>;
52
60
  services(input: {
53
61
  credentials: HostedOAuthCredentialAccess<TCredential>;
62
+ identity: HostedOAuthIdentity;
54
63
  }): Promise<Partial<TServices>> | Partial<TServices>;
55
64
  }
56
65
  export interface HostedOAuthInteractionAdapter<TCredential = unknown> {
@@ -106,7 +115,7 @@ export interface HostedOAuthRuntime<TServices extends object = object> {
106
115
  mcpPath: string;
107
116
  oauth: TinyHttpMcpServerOAuthOptions;
108
117
  requestHandler: HttpAdditionalRequestHandler;
109
- requestServices(subject: string): Promise<Partial<TServices>>;
118
+ requestServices(identity: HostedOAuthIdentity): Promise<Partial<TServices>>;
110
119
  }
111
120
  export declare function createInMemoryHostedOAuthStorage<TCredential = unknown>(options: {
112
121
  development: true;
@@ -1,4 +1,4 @@
1
- import { createHmac, generateKeyPairSync, randomBytes } from "node:crypto";
1
+ import { createHash, createHmac, generateKeyPairSync, randomBytes } from "node:crypto";
2
2
  import { exportJWK } from "jose";
3
3
  import { createAuthorizationInteractionSecurity, createInMemoryAuthorizationServerStore, createOAuthAuthorizationServer, verifyAuthorizationInteractionCsrf } from "mcp-oauth-server";
4
4
  export function isHostedOAuthConfiguration(value) {
@@ -12,6 +12,9 @@ function normalizePublicUrl(value) {
12
12
  if (url.username.length > 0 || url.password.length > 0) {
13
13
  throw new Error("hosted OAuth publicUrl must not contain credentials.");
14
14
  }
15
+ if (url.pathname === "/" || url.pathname.endsWith("/")) {
16
+ throw new Error("hosted OAuth publicUrl must contain a non-root path without a trailing slash.");
17
+ }
15
18
  return url;
16
19
  }
17
20
  function configurationErrors(config, production) {
@@ -117,6 +120,14 @@ function renderLogin(providerName, fields, transaction, csrfToken, error) {
117
120
  .join("");
118
121
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Connect ${escapeHtml(providerName)}</title><style>body{font:16px system-ui;max-width:28rem;margin:10vh auto;padding:1rem;color:#171717}form{display:grid;gap:1rem}label{display:grid;gap:.35rem}input,button{font:inherit;padding:.7rem}button{cursor:pointer}${error === undefined ? "" : ".error{color:#b42318}"}</style></head><body><h1>Connect ${escapeHtml(providerName)}</h1>${error === undefined ? "" : `<p class="error">${escapeHtml(error)}</p>`}<form method="post" action="/oauth/connect"><input type="hidden" name="transaction" value="${escapeHtml(transaction.id)}"><input type="hidden" name="csrf" value="${escapeHtml(csrfToken)}">${controls}<button type="submit">Connect</button></form></body></html>`;
119
122
  }
123
+ function loginContentSecurityPolicy(transaction) {
124
+ const callbackOrigin = new URL(transaction.redirectUri).origin;
125
+ return `default-src 'none'; style-src 'unsafe-inline'; form-action 'self' ${callbackOrigin}; base-uri 'none'; frame-ancestors 'none'`;
126
+ }
127
+ function interactionCookieName(transactionId) {
128
+ const suffix = createHash("sha256").update(transactionId).digest("base64url").slice(0, 22);
129
+ return `__Host-mcp_oauth_csrf_${suffix}`;
130
+ }
120
131
  function writeWebResponse(response, webResponse) {
121
132
  webResponse.headers.forEach((value, name) => response.setHeader(name, value));
122
133
  response.statusCode = webResponse.status;
@@ -172,12 +183,14 @@ export async function prepareHostedOAuthRuntime(config) {
172
183
  if (customInteraction !== undefined) {
173
184
  return customInteraction.start({ request, transaction });
174
185
  }
175
- const security = createAuthorizationInteractionSecurity();
186
+ const security = createAuthorizationInteractionSecurity({
187
+ cookieName: interactionCookieName(transaction.id)
188
+ });
176
189
  return new Response(renderLogin(displayName, fields, transaction, security.csrfToken), {
177
190
  status: 200,
178
191
  headers: {
179
192
  "content-type": "text/html; charset=utf-8",
180
- "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
193
+ "content-security-policy": loginContentSecurityPolicy(transaction),
181
194
  "cache-control": "no-store",
182
195
  "referrer-policy": "no-referrer",
183
196
  "x-content-type-options": "nosniff",
@@ -191,6 +204,7 @@ export async function prepareHostedOAuthRuntime(config) {
191
204
  issuer: prepared.issuer.href,
192
205
  resources: [prepared.publicUrl.href],
193
206
  scopesSupported: prepared.scopes,
207
+ defaultScopes: prepared.scopes,
194
208
  signingKey: await config.storage.signingKey(),
195
209
  additionalPublicJwks: config.advanced?.additionalPublicJwks,
196
210
  store: config.storage.authorizationServer,
@@ -203,8 +217,21 @@ export async function prepareHostedOAuthRuntime(config) {
203
217
  const requestHandler = async (request, response) => {
204
218
  const url = new URL(request.url ?? "/", prepared.issuer);
205
219
  if (request.method === "GET" && url.pathname === "/healthz") {
206
- response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
207
- response.end('{"ok":true}');
220
+ try {
221
+ await config.storage.healthCheck?.();
222
+ response.writeHead(200, {
223
+ "content-type": "application/json",
224
+ "cache-control": "no-store"
225
+ });
226
+ response.end('{"ok":true}');
227
+ }
228
+ catch {
229
+ response.writeHead(503, {
230
+ "content-type": "application/json",
231
+ "cache-control": "no-store"
232
+ });
233
+ response.end('{"ok":false}');
234
+ }
208
235
  return true;
209
236
  }
210
237
  if (customInteraction?.paths.includes(url.pathname) === true) {
@@ -213,11 +240,11 @@ export async function prepareHostedOAuthRuntime(config) {
213
240
  request: toWebRequest(request, prepared.issuer, body),
214
241
  async complete({ transactionId, accountId, credential }) {
215
242
  const subject = await config.storage.resolveSubject(config.provider.name, accountId);
243
+ await config.storage.credentials.set(subject, credential);
216
244
  const completed = await authorizationServer.completeAuthorization({
217
245
  transactionId,
218
246
  subject
219
247
  });
220
- await config.storage.credentials.set(subject, credential);
221
248
  await config.storage.interactions.delete(transactionId);
222
249
  return new Response(null, {
223
250
  status: 303,
@@ -241,7 +268,8 @@ export async function prepareHostedOAuthRuntime(config) {
241
268
  if (transaction === undefined ||
242
269
  !verifyAuthorizationInteractionCsrf({
243
270
  cookieHeader: request.headers.cookie ?? null,
244
- submittedToken: csrf
271
+ submittedToken: csrf,
272
+ cookieName: interactionCookieName(transactionId)
245
273
  }) ||
246
274
  transaction.expiresAt <= Date.now()) {
247
275
  response.writeHead(400, {
@@ -266,11 +294,11 @@ export async function prepareHostedOAuthRuntime(config) {
266
294
  if (connected.accountId.trim().length === 0)
267
295
  throw new Error("Provider returned an empty accountId.");
268
296
  const subject = await config.storage.resolveSubject(config.provider.name, connected.accountId);
297
+ await config.storage.credentials.set(subject, connected.credential);
269
298
  const completed = await authorizationServer.completeAuthorization({
270
299
  transactionId,
271
300
  subject
272
301
  });
273
- await config.storage.credentials.set(subject, connected.credential);
274
302
  await config.storage.interactions.delete(transactionId);
275
303
  response.writeHead(303, {
276
304
  location: completed.redirectUrl.href,
@@ -286,7 +314,7 @@ export async function prepareHostedOAuthRuntime(config) {
286
314
  : "Sign-in failed. Please try again.";
287
315
  response.writeHead(400, {
288
316
  "content-type": "text/html; charset=utf-8",
289
- "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
317
+ "content-security-policy": loginContentSecurityPolicy(transaction),
290
318
  "cache-control": "no-store",
291
319
  "referrer-policy": "no-referrer"
292
320
  });
@@ -332,9 +360,10 @@ export async function prepareHostedOAuthRuntime(config) {
332
360
  }
333
361
  },
334
362
  requestHandler,
335
- async requestServices(subject) {
336
- await credentialsFor(config.storage, subject).read();
337
- return config.provider.services({ credentials: credentialsFor(config.storage, subject) });
363
+ async requestServices(identity) {
364
+ const credentials = credentialsFor(config.storage, identity.subject);
365
+ await credentials.read();
366
+ return config.provider.services({ credentials, identity });
338
367
  }
339
368
  };
340
369
  }
package/dist/http.js CHANGED
@@ -91,12 +91,19 @@ export async function createHTTPMCPServer(roots, options) {
91
91
  sessionIdGenerator: undefined,
92
92
  enableJsonResponse: true,
93
93
  requestServices: async (context) => {
94
- const subject = context.auth?.subject;
95
- if (subject === undefined)
94
+ const auth = context.auth;
95
+ const subject = auth?.subject;
96
+ if (auth === undefined || subject === undefined)
96
97
  throw new Error("Hosted OAuth request is missing a verified subject.");
97
98
  return {
98
99
  ...(applicationServices === undefined ? {} : await applicationServices(context)),
99
- ...(await runtime.requestServices(subject))
100
+ ...(await runtime.requestServices({
101
+ issuer: auth.issuer,
102
+ subject,
103
+ clientId: auth.clientId,
104
+ scopes: auth.scopes,
105
+ resource: auth.resource.href
106
+ }))
100
107
  };
101
108
  },
102
109
  requestHandler: runtime.requestHandler
@@ -118,10 +125,23 @@ export async function createHTTPMCPServer(roots, options) {
118
125
  }
119
126
  if (hostedMcpPath !== undefined) {
120
127
  const listenHttp = server.listenHttp.bind(server);
121
- server.listenHttp = (listenOptions = {}) => listenHttp({
122
- path: hostedMcpPath,
123
- ...listenOptions
124
- });
128
+ server.listenHttp = async (listenOptions = {}) => {
129
+ if (listenOptions.path !== undefined) {
130
+ const withLeadingSlash = listenOptions.path.startsWith("/")
131
+ ? listenOptions.path
132
+ : `/${listenOptions.path}`;
133
+ const requestedPath = withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/")
134
+ ? withLeadingSlash.slice(0, -1)
135
+ : withLeadingSlash;
136
+ if (requestedPath !== hostedMcpPath) {
137
+ throw new Error(`Hosted OAuth MCP path ${JSON.stringify(requestedPath)} conflicts with publicUrl path ${JSON.stringify(hostedMcpPath)}.`);
138
+ }
139
+ }
140
+ return listenHttp({
141
+ ...listenOptions,
142
+ path: hostedMcpPath
143
+ });
144
+ };
125
145
  }
126
146
  return server;
127
147
  }
@@ -99,6 +99,7 @@ export interface OAuthAuthorizationServerOptions {
99
99
  issuer: string;
100
100
  resources: readonly string[];
101
101
  scopesSupported?: readonly string[];
102
+ defaultScopes?: readonly string[];
102
103
  signingKey: OAuthAuthorizationServerSigningKey;
103
104
  additionalPublicJwks?: readonly JWK[];
104
105
  store: AuthorizationServerStore;
@@ -249,6 +249,13 @@ export function createOAuthAuthorizationServer(options) {
249
249
  const issuer = validateIssuer(options.issuer);
250
250
  const resources = validateResources(options.resources);
251
251
  const scopesSupported = options.scopesSupported === undefined ? undefined : new Set(options.scopesSupported);
252
+ const defaultScopes = [...new Set(options.defaultScopes ?? [])];
253
+ if (defaultScopes.some((scope) => scope.length === 0 || /[\u0000-\u0020\u007f]/u.test(scope))) {
254
+ throw new Error("default scopes must contain valid scope names.");
255
+ }
256
+ if (scopesSupported !== undefined && defaultScopes.some((scope) => !scopesSupported.has(scope))) {
257
+ throw new Error("default scopes must be included in supported scopes.");
258
+ }
252
259
  const now = options.now ?? Date.now;
253
260
  const randomToken = options.randomToken ?? opaqueToken;
254
261
  const accessTokenTtlMs = (options.accessTokenTtlSeconds ?? 300) * 1000;
@@ -365,7 +372,12 @@ export function createOAuthAuthorizationServer(options) {
365
372
  if (url.searchParams.get("code_challenge_method") !== "S256") {
366
373
  throw new OAuthProtocolError("invalid_request", "code_challenge_method must be S256.");
367
374
  }
368
- const requestedScopes = parseScopes(url.searchParams.get("scope"));
375
+ const requestedScopes = url.searchParams.has("scope")
376
+ ? parseScopes(url.searchParams.get("scope"))
377
+ : [...defaultScopes];
378
+ if (defaultScopes.length > 0 && requestedScopes.length === 0) {
379
+ throw new OAuthProtocolError("invalid_scope", "scope must not be empty.");
380
+ }
369
381
  if (scopesSupported !== undefined &&
370
382
  requestedScopes.some((scope) => !scopesSupported.has(scope))) {
371
383
  throw new OAuthProtocolError("invalid_scope", "One or more requested scopes are not supported.");
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.5",
21
+ "version": "0.1.6",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -40,7 +40,7 @@ export function createProtectedResourceMetadataRouter(options) {
40
40
  if (path === "/") {
41
41
  return [PROTECTED_RESOURCE_METADATA_PATH];
42
42
  }
43
- return [`${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
43
+ return [PROTECTED_RESOURCE_METADATA_PATH, `${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
44
44
  })();
45
45
  return (req, res, next) => {
46
46
  if (req.method !== "GET" || !metadataPaths.includes(req.path)) {
@@ -19,7 +19,7 @@ function getProtectedResourceMetadataPaths(path) {
19
19
  if (path === "/") {
20
20
  return [PROTECTED_RESOURCE_METADATA_PATH];
21
21
  }
22
- return [`${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
22
+ return [PROTECTED_RESOURCE_METADATA_PATH, `${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
23
23
  }
24
24
  function formatHostnameForUrl(hostname) {
25
25
  if (hostname.includes(":") && !hostname.startsWith("[")) {
@@ -252,6 +252,9 @@ export function createHttpServer(options) {
252
252
  };
253
253
  };
254
254
  httpServer.handleRequest = async (req, res) => {
255
+ if (options.requestHandler !== undefined && (await options.requestHandler(req, res))) {
256
+ return;
257
+ }
255
258
  const { baseUrl } = req;
256
259
  const protectedResourcePath = typeof baseUrl === "string" && baseUrl.length > 0 ? baseUrl : "/mcp";
257
260
  if (!(await authorizeHttpRequest(req, res, protectedResourcePath))) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.142",
11
+ "version": "0.0.144",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.142",
3
+ "version": "0.0.144",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.142",
3
+ "version": "0.0.144",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -158,7 +158,7 @@
158
158
  "yaml"
159
159
  ],
160
160
  "optionalDependencies": {
161
- "toolcraft-schema": "0.0.142",
161
+ "toolcraft-schema": "0.0.144",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",