toolcraft 0.0.142 → 0.0.143
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 +3 -3
- package/dist/composition.json +3 -3
- package/dist/http-hosted-oauth.compile-check.js +11 -1
- package/dist/http-hosted-oauth.d.ts +9 -1
- package/dist/http-hosted-oauth.js +24 -8
- package/dist/http.js +27 -7
- package/node_modules/mcp-oauth-server/dist/index.d.ts +1 -0
- package/node_modules/mcp-oauth-server/dist/index.js +13 -1
- package/node_modules/tiny-http-mcp-server/dist/composition.json +1 -1
- package/node_modules/tiny-http-mcp-server/dist/express-middleware.js +1 -1
- package/node_modules/tiny-http-mcp-server/dist/http-server.js +4 -1
- package/node_modules/tiny-http-mcp-server/package.json +1 -1
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
package/composition.json
CHANGED
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"name": "tiny-http-mcp-server",
|
|
101
|
-
"version": "0.1.
|
|
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.
|
|
116
|
+
"version": "0.0.143",
|
|
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.
|
|
126
|
+
"version": "0.0.143",
|
|
127
127
|
"license": "MIT"
|
|
128
128
|
},
|
|
129
129
|
{
|
package/dist/composition.json
CHANGED
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"name": "tiny-http-mcp-server",
|
|
101
|
-
"version": "0.1.
|
|
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.
|
|
116
|
+
"version": "0.0.143",
|
|
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.
|
|
126
|
+
"version": "0.0.143",
|
|
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
|
}
|
|
@@ -38,6 +38,13 @@ export interface HostedOAuthCredentialAccess<TCredential = unknown> {
|
|
|
38
38
|
update(update: (credential: TCredential) => Promise<TCredential> | TCredential): Promise<TCredential>;
|
|
39
39
|
delete(): Promise<void>;
|
|
40
40
|
}
|
|
41
|
+
export interface HostedOAuthIdentity {
|
|
42
|
+
issuer: string;
|
|
43
|
+
subject: string;
|
|
44
|
+
clientId: string;
|
|
45
|
+
scopes: readonly string[];
|
|
46
|
+
resource: string;
|
|
47
|
+
}
|
|
41
48
|
export interface HostedOAuthProvider<TCredential = unknown, TServices extends object = object> {
|
|
42
49
|
name: string;
|
|
43
50
|
login?: {
|
|
@@ -51,6 +58,7 @@ export interface HostedOAuthProvider<TCredential = unknown, TServices extends ob
|
|
|
51
58
|
}>;
|
|
52
59
|
services(input: {
|
|
53
60
|
credentials: HostedOAuthCredentialAccess<TCredential>;
|
|
61
|
+
identity: HostedOAuthIdentity;
|
|
54
62
|
}): Promise<Partial<TServices>> | Partial<TServices>;
|
|
55
63
|
}
|
|
56
64
|
export interface HostedOAuthInteractionAdapter<TCredential = unknown> {
|
|
@@ -106,7 +114,7 @@ export interface HostedOAuthRuntime<TServices extends object = object> {
|
|
|
106
114
|
mcpPath: string;
|
|
107
115
|
oauth: TinyHttpMcpServerOAuthOptions;
|
|
108
116
|
requestHandler: HttpAdditionalRequestHandler;
|
|
109
|
-
requestServices(
|
|
117
|
+
requestServices(identity: HostedOAuthIdentity): Promise<Partial<TServices>>;
|
|
110
118
|
}
|
|
111
119
|
export declare function createInMemoryHostedOAuthStorage<TCredential = unknown>(options: {
|
|
112
120
|
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":
|
|
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,
|
|
@@ -241,7 +255,8 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
241
255
|
if (transaction === undefined ||
|
|
242
256
|
!verifyAuthorizationInteractionCsrf({
|
|
243
257
|
cookieHeader: request.headers.cookie ?? null,
|
|
244
|
-
submittedToken: csrf
|
|
258
|
+
submittedToken: csrf,
|
|
259
|
+
cookieName: interactionCookieName(transactionId)
|
|
245
260
|
}) ||
|
|
246
261
|
transaction.expiresAt <= Date.now()) {
|
|
247
262
|
response.writeHead(400, {
|
|
@@ -286,7 +301,7 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
286
301
|
: "Sign-in failed. Please try again.";
|
|
287
302
|
response.writeHead(400, {
|
|
288
303
|
"content-type": "text/html; charset=utf-8",
|
|
289
|
-
"content-security-policy":
|
|
304
|
+
"content-security-policy": loginContentSecurityPolicy(transaction),
|
|
290
305
|
"cache-control": "no-store",
|
|
291
306
|
"referrer-policy": "no-referrer"
|
|
292
307
|
});
|
|
@@ -332,9 +347,10 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
332
347
|
}
|
|
333
348
|
},
|
|
334
349
|
requestHandler,
|
|
335
|
-
async requestServices(
|
|
336
|
-
|
|
337
|
-
|
|
350
|
+
async requestServices(identity) {
|
|
351
|
+
const credentials = credentialsFor(config.storage, identity.subject);
|
|
352
|
+
await credentials.read();
|
|
353
|
+
return config.provider.services({ credentials, identity });
|
|
338
354
|
}
|
|
339
355
|
};
|
|
340
356
|
}
|
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
|
|
95
|
-
|
|
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(
|
|
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 = {}) =>
|
|
122
|
-
path
|
|
123
|
-
|
|
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 =
|
|
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.");
|
|
@@ -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))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.143",
|
|
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.
|
|
161
|
+
"toolcraft-schema": "0.0.143",
|
|
162
162
|
"toolcraft-design": "*",
|
|
163
163
|
"@poe-code/frontmatter": "*",
|
|
164
164
|
"@poe-code/agent-mcp-config": "*",
|