@solvapay/mcp 0.1.0 → 0.2.0

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.
@@ -0,0 +1,565 @@
1
+ import {
2
+ applyHideToolsByAudience,
3
+ buildSolvaPayMcpServer,
4
+ registerPayableTool
5
+ } from "../chunk-3VDAADZU.js";
6
+
7
+ // src/fetch/oauth-bridge.ts
8
+ import {
9
+ getOAuthAuthorizationServerResponse,
10
+ getOAuthProtectedResourceResponse,
11
+ resolveOAuthPaths,
12
+ withoutTrailingSlash as withoutTrailingSlash2
13
+ } from "@solvapay/mcp-core";
14
+
15
+ // src/fetch/cors.ts
16
+ import { withoutTrailingSlash } from "@solvapay/mcp-core";
17
+ var NATIVE_CLIENT_ORIGIN_REGEX = /^(cursor|vscode|vscode-webview|claude):\/\/.+$/;
18
+ function isNativeClientOrigin(origin) {
19
+ if (!origin) return false;
20
+ return NATIVE_CLIENT_ORIGIN_REGEX.test(origin);
21
+ }
22
+ function applyNativeCors(reqHeaders, resHeaders) {
23
+ const origin = reqHeaders.get("origin");
24
+ if (!origin) return;
25
+ if (isNativeClientOrigin(origin)) {
26
+ resHeaders.set("Access-Control-Allow-Origin", origin);
27
+ resHeaders.append("Vary", "Origin");
28
+ }
29
+ }
30
+ function corsPreflight(req) {
31
+ const reqHeaders = req.headers;
32
+ const requestedMethod = reqHeaders.get("access-control-request-method") ?? "POST";
33
+ const requestedHeaders = reqHeaders.get("access-control-request-headers") ?? "authorization, content-type";
34
+ const headers = new Headers();
35
+ applyNativeCors(reqHeaders, headers);
36
+ headers.set("Access-Control-Allow-Methods", `${requestedMethod}, OPTIONS`);
37
+ headers.set("Access-Control-Allow-Headers", requestedHeaders);
38
+ headers.set("Access-Control-Max-Age", "600");
39
+ return new Response(null, { status: 204, headers });
40
+ }
41
+ function authChallenge(req, options) {
42
+ const {
43
+ publicBaseUrl,
44
+ protectedResourcePath = "/.well-known/oauth-protected-resource",
45
+ jsonRpcId = null
46
+ } = options;
47
+ const headers = new Headers();
48
+ applyNativeCors(req.headers, headers);
49
+ headers.set("Access-Control-Expose-Headers", "WWW-Authenticate");
50
+ headers.set(
51
+ "WWW-Authenticate",
52
+ `Bearer resource_metadata="${withoutTrailingSlash(publicBaseUrl)}${protectedResourcePath}"`
53
+ );
54
+ headers.set("Content-Type", "application/json");
55
+ const body = {
56
+ jsonrpc: "2.0",
57
+ id: jsonRpcId,
58
+ error: { code: -32001, message: "Unauthorized" }
59
+ };
60
+ return new Response(JSON.stringify(body), { status: 401, headers });
61
+ }
62
+ function resolveBearer(req) {
63
+ const header = req.headers.get("authorization");
64
+ if (!header) return null;
65
+ const match = /^\s*Bearer\s+(.+?)\s*$/i.exec(header);
66
+ return match ? match[1] : null;
67
+ }
68
+
69
+ // src/fetch/oauth-bridge.ts
70
+ function hasOAuthErrorShape(body) {
71
+ return body !== null && typeof body === "object" && typeof body.error === "string";
72
+ }
73
+ function extractZodErrors(body) {
74
+ const errs = body.errors;
75
+ if (!Array.isArray(errs)) return [];
76
+ return errs.filter((e) => !!e && typeof e === "object");
77
+ }
78
+ function deriveOAuthErrorCode(status, nestBody) {
79
+ if (status === 401 || status === 403) return "invalid_client";
80
+ if (status >= 500) return "server_error";
81
+ const zodErrors = extractZodErrors(nestBody);
82
+ const touches = (field) => zodErrors.some((e) => {
83
+ const path = e.path;
84
+ return Array.isArray(path) && path.includes(field);
85
+ });
86
+ if (touches("grant_type")) {
87
+ const grantTypeErr = zodErrors.find((e) => {
88
+ const path = e.path;
89
+ return Array.isArray(path) && path.includes("grant_type");
90
+ });
91
+ const received = grantTypeErr && grantTypeErr.received;
92
+ if (received !== "undefined" && received !== void 0 && received !== "") {
93
+ return "unsupported_grant_type";
94
+ }
95
+ return "invalid_request";
96
+ }
97
+ if (touches("code") || touches("refresh_token")) return "invalid_grant";
98
+ if (touches("scope")) return "invalid_scope";
99
+ if (touches("client_id") || touches("client_secret")) return "invalid_client";
100
+ return "invalid_request";
101
+ }
102
+ function buildErrorDescription(nestBody) {
103
+ const zodErrors = extractZodErrors(nestBody);
104
+ if (zodErrors.length > 0) {
105
+ const parts = zodErrors.map((e) => {
106
+ const path = e.path;
107
+ const message2 = e.message;
108
+ const pathStr = Array.isArray(path) ? path.filter((p) => typeof p === "string").join(".") : "";
109
+ const msgStr = typeof message2 === "string" ? message2 : "";
110
+ if (pathStr && msgStr) return `${pathStr}: ${msgStr}`;
111
+ return pathStr || msgStr;
112
+ }).filter(Boolean);
113
+ if (parts.length > 0) return parts.join("; ");
114
+ }
115
+ const message = nestBody.message;
116
+ if (typeof message === "string") return message;
117
+ if (Array.isArray(message)) {
118
+ const strings = message.filter((m) => typeof m === "string");
119
+ if (strings.length > 0) return strings.join("; ");
120
+ }
121
+ return void 0;
122
+ }
123
+ function toOAuthErrorBody(body, text, status) {
124
+ if (hasOAuthErrorShape(body)) return body;
125
+ if (body && typeof body === "object") {
126
+ const nestBody = body;
127
+ const error = deriveOAuthErrorCode(status, nestBody);
128
+ const error_description = buildErrorDescription(nestBody);
129
+ return error_description ? { error, error_description } : { error };
130
+ }
131
+ const fallbackError = status >= 500 ? "server_error" : "invalid_request";
132
+ const description = typeof text === "string" && text.length > 0 && text.length < 500 ? text : void 0;
133
+ return description ? { error: fallbackError, error_description: description } : { error: fallbackError };
134
+ }
135
+ async function parseUpstreamJson(response) {
136
+ const text = await response.text();
137
+ if (!text) return { body: {}, text: "" };
138
+ try {
139
+ return { body: JSON.parse(text), text };
140
+ } catch {
141
+ return { body: text, text };
142
+ }
143
+ }
144
+ function upstreamUnreachable(req) {
145
+ const headers = new Headers({ "content-type": "application/json" });
146
+ applyNativeCors(req.headers, headers);
147
+ return new Response(JSON.stringify({ error: "upstream_unreachable" }), { status: 502, headers });
148
+ }
149
+ function corsResponse(req, response) {
150
+ const headers = new Headers(response.headers);
151
+ applyNativeCors(req.headers, headers);
152
+ return new Response(response.body, { status: response.status, headers });
153
+ }
154
+ function jsonResponse(req, status, body) {
155
+ const headers = new Headers({ "content-type": "application/json" });
156
+ applyNativeCors(req.headers, headers);
157
+ return new Response(JSON.stringify(body), { status, headers });
158
+ }
159
+ function emptyResponse(req, status) {
160
+ const headers = new Headers();
161
+ applyNativeCors(req.headers, headers);
162
+ return new Response(null, { status, headers });
163
+ }
164
+ function pathOf(req) {
165
+ return new URL(req.url).pathname;
166
+ }
167
+ function queryOf(req) {
168
+ const search = new URL(req.url).search;
169
+ return search || "";
170
+ }
171
+ function createProtectedResourceHandler(options) {
172
+ const path = options.protectedResourcePath ?? "/.well-known/oauth-protected-resource";
173
+ return async (req) => {
174
+ if (req.method !== "GET" || pathOf(req) !== path) return null;
175
+ return jsonResponse(req, 200, getOAuthProtectedResourceResponse(options.publicBaseUrl));
176
+ };
177
+ }
178
+ function createAuthorizationServerHandler(options) {
179
+ const path = options.authorizationServerPath ?? "/.well-known/oauth-authorization-server";
180
+ const resolvedPaths = resolveOAuthPaths(options.paths);
181
+ return async (req) => {
182
+ if (req.method !== "GET" || pathOf(req) !== path) return null;
183
+ if (!options.productRef) {
184
+ return jsonResponse(req, 500, { error: "SOLVAPAY_PRODUCT_REF missing" });
185
+ }
186
+ return jsonResponse(
187
+ req,
188
+ 200,
189
+ getOAuthAuthorizationServerResponse({
190
+ publicBaseUrl: options.publicBaseUrl,
191
+ paths: resolvedPaths
192
+ })
193
+ );
194
+ };
195
+ }
196
+ function createOpenidNotFoundHandler() {
197
+ return async (req) => {
198
+ if (req.method !== "GET" || pathOf(req) !== "/.well-known/openid-configuration") return null;
199
+ return emptyResponse(req, 404);
200
+ };
201
+ }
202
+ function createOAuthRegisterHandler(options) {
203
+ const path = options.path ?? "/oauth/register";
204
+ const api = withoutTrailingSlash2(options.apiBaseUrl);
205
+ const upstream = `${api}/v1/customer/auth/register?product_ref=${encodeURIComponent(options.productRef)}`;
206
+ return async (req) => {
207
+ if (pathOf(req) !== path) return null;
208
+ if (req.method === "OPTIONS") return corsPreflight(req);
209
+ if (req.method !== "POST") return null;
210
+ const body = await req.text();
211
+ try {
212
+ const upstreamResponse = await fetch(upstream, {
213
+ method: "POST",
214
+ headers: { "content-type": req.headers.get("content-type") ?? "application/json" },
215
+ body
216
+ });
217
+ return corsResponse(req, upstreamResponse);
218
+ } catch {
219
+ return upstreamUnreachable(req);
220
+ }
221
+ };
222
+ }
223
+ function createOAuthAuthorizeHandler(options) {
224
+ const path = options.path ?? "/oauth/authorize";
225
+ const api = withoutTrailingSlash2(options.apiBaseUrl);
226
+ return async (req) => {
227
+ if (pathOf(req) !== path) return null;
228
+ if (req.method === "OPTIONS") return corsPreflight(req);
229
+ if (req.method !== "GET") return null;
230
+ const query = queryOf(req);
231
+ const location = `${api}/v1/customer/auth/authorize${query}`;
232
+ const headers = new Headers({ Location: location });
233
+ applyNativeCors(req.headers, headers);
234
+ return new Response(null, { status: 302, headers });
235
+ };
236
+ }
237
+ async function proxyFormEndpoint(req, upstreamUrl, normalizeErrors) {
238
+ const rawBody = await req.text();
239
+ const contentType = req.headers.get("content-type") ?? "application/x-www-form-urlencoded";
240
+ const headers = { "content-type": contentType };
241
+ const authorization = req.headers.get("authorization");
242
+ if (authorization) headers.authorization = authorization;
243
+ try {
244
+ const upstreamResponse = await fetch(upstreamUrl, {
245
+ method: "POST",
246
+ headers,
247
+ body: rawBody
248
+ });
249
+ if (!normalizeErrors || upstreamResponse.ok || upstreamResponse.status === 204) {
250
+ return corsResponse(req, upstreamResponse);
251
+ }
252
+ const { body, text } = await parseUpstreamJson(upstreamResponse);
253
+ const normalized = toOAuthErrorBody(body, text, upstreamResponse.status);
254
+ return jsonResponse(req, upstreamResponse.status, normalized);
255
+ } catch {
256
+ return upstreamUnreachable(req);
257
+ }
258
+ }
259
+ function createOAuthTokenHandler(options) {
260
+ const path = options.path ?? "/oauth/token";
261
+ const upstream = `${withoutTrailingSlash2(options.apiBaseUrl)}/v1/customer/auth/token`;
262
+ return async (req) => {
263
+ if (pathOf(req) !== path) return null;
264
+ if (req.method === "OPTIONS") return corsPreflight(req);
265
+ if (req.method !== "POST") return null;
266
+ return proxyFormEndpoint(
267
+ req,
268
+ upstream,
269
+ /* normalizeErrors */
270
+ true
271
+ );
272
+ };
273
+ }
274
+ function createOAuthRevokeHandler(options) {
275
+ const path = options.path ?? "/oauth/revoke";
276
+ const upstream = `${withoutTrailingSlash2(options.apiBaseUrl)}/v1/customer/auth/revoke`;
277
+ return async (req) => {
278
+ if (pathOf(req) !== path) return null;
279
+ if (req.method === "OPTIONS") return corsPreflight(req);
280
+ if (req.method !== "POST") return null;
281
+ return proxyFormEndpoint(
282
+ req,
283
+ upstream,
284
+ /* normalizeErrors */
285
+ true
286
+ );
287
+ };
288
+ }
289
+ function createOAuthFetchRouter(options) {
290
+ const paths = resolveOAuthPaths(options.oauthPaths);
291
+ const handlers = [
292
+ createOpenidNotFoundHandler(),
293
+ createProtectedResourceHandler({
294
+ publicBaseUrl: options.publicBaseUrl,
295
+ protectedResourcePath: options.protectedResourcePath
296
+ }),
297
+ createAuthorizationServerHandler({
298
+ publicBaseUrl: options.publicBaseUrl,
299
+ authorizationServerPath: options.authorizationServerPath,
300
+ paths,
301
+ productRef: options.productRef
302
+ }),
303
+ createOAuthRegisterHandler({
304
+ apiBaseUrl: options.apiBaseUrl,
305
+ productRef: options.productRef,
306
+ path: paths.register
307
+ }),
308
+ createOAuthAuthorizeHandler({ apiBaseUrl: options.apiBaseUrl, path: paths.authorize }),
309
+ createOAuthTokenHandler({ apiBaseUrl: options.apiBaseUrl, path: paths.token }),
310
+ createOAuthRevokeHandler({ apiBaseUrl: options.apiBaseUrl, path: paths.revoke })
311
+ ];
312
+ return async (req) => {
313
+ for (const handler of handlers) {
314
+ const response = await handler(req);
315
+ if (response) return response;
316
+ }
317
+ return null;
318
+ };
319
+ }
320
+
321
+ // src/fetch/handler.ts
322
+ import {
323
+ buildAuthInfoFromBearer,
324
+ McpBearerAuthError
325
+ } from "@solvapay/mcp-core";
326
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
327
+ function defaultSessionIdGenerator() {
328
+ const c = globalThis.crypto;
329
+ if (c?.randomUUID) return c.randomUUID();
330
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
331
+ const r = Math.floor(Math.random() * 16);
332
+ const v = ch === "x" ? r : r & 3 | 8;
333
+ return v.toString(16);
334
+ });
335
+ }
336
+ function getJsonRpcId(body) {
337
+ if (body && typeof body === "object" && "id" in body) {
338
+ const id = body.id;
339
+ return id ?? null;
340
+ }
341
+ return null;
342
+ }
343
+ async function readJsonRpcId(req) {
344
+ try {
345
+ const clone = req.clone();
346
+ const body = await clone.json();
347
+ return getJsonRpcId(body);
348
+ } catch {
349
+ return null;
350
+ }
351
+ }
352
+ function createSolvaPayMcpFetchHandler(options) {
353
+ const {
354
+ server,
355
+ publicBaseUrl,
356
+ apiBaseUrl,
357
+ productRef,
358
+ mcpPath = "/mcp",
359
+ requireAuth = true,
360
+ authInfo,
361
+ protectedResourcePath,
362
+ authorizationServerPath,
363
+ oauthPaths,
364
+ mode = "sse-stateful",
365
+ buildTransport,
366
+ sessionIdGenerator
367
+ } = options;
368
+ const oauthRouter = createOAuthFetchRouter({
369
+ publicBaseUrl,
370
+ apiBaseUrl,
371
+ productRef,
372
+ protectedResourcePath,
373
+ authorizationServerPath,
374
+ oauthPaths
375
+ });
376
+ const makeTransport = () => {
377
+ if (buildTransport) return buildTransport();
378
+ if (mode === "json-stateless") {
379
+ return new WebStandardStreamableHTTPServerTransport({
380
+ sessionIdGenerator: void 0,
381
+ enableJsonResponse: true
382
+ });
383
+ }
384
+ if (mode === "sse-stateless") {
385
+ return new WebStandardStreamableHTTPServerTransport({
386
+ sessionIdGenerator: void 0
387
+ });
388
+ }
389
+ return new WebStandardStreamableHTTPServerTransport({
390
+ sessionIdGenerator: sessionIdGenerator ?? defaultSessionIdGenerator
391
+ });
392
+ };
393
+ let serverMutex = Promise.resolve();
394
+ return async (req) => {
395
+ const url = new URL(req.url);
396
+ const pathname = url.pathname;
397
+ if (req.method === "OPTIONS" && pathname === mcpPath) {
398
+ return corsPreflight(req);
399
+ }
400
+ const oauthResponse = await oauthRouter(req);
401
+ if (oauthResponse) return oauthResponse;
402
+ if (pathname !== mcpPath) {
403
+ return new Response("not_found", { status: 404 });
404
+ }
405
+ if (req.method && req.method !== "POST" && req.method !== "OPTIONS") {
406
+ const headers = new Headers({ Allow: "POST, OPTIONS" });
407
+ applyNativeCors(req.headers, headers);
408
+ return new Response(null, { status: 405, headers });
409
+ }
410
+ const authHeader = req.headers.get("authorization");
411
+ let resolvedAuthInfo = null;
412
+ if (authHeader || requireAuth) {
413
+ try {
414
+ resolvedAuthInfo = buildAuthInfoFromBearer(authHeader, authInfo);
415
+ if (!resolvedAuthInfo) {
416
+ throw new McpBearerAuthError("Missing bearer token");
417
+ }
418
+ } catch {
419
+ const jsonRpcId = await readJsonRpcId(req);
420
+ return authChallenge(req, {
421
+ publicBaseUrl,
422
+ protectedResourcePath,
423
+ jsonRpcId
424
+ });
425
+ }
426
+ }
427
+ const previous = serverMutex;
428
+ let releaseMutex = () => {
429
+ };
430
+ serverMutex = new Promise((resolve) => {
431
+ releaseMutex = resolve;
432
+ });
433
+ await previous;
434
+ const transport = makeTransport();
435
+ try {
436
+ await server.connect(transport);
437
+ const response = await transport.handleRequest(
438
+ req,
439
+ resolvedAuthInfo ? {
440
+ // `AuthInfo` from the SDK is structurally identical to our
441
+ // envelope — cast away the brand so the types line up.
442
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
443
+ authInfo: resolvedAuthInfo
444
+ } : void 0
445
+ );
446
+ const merged = new Headers(response.headers);
447
+ applyNativeCors(req.headers, merged);
448
+ return new Response(response.body, { status: response.status, headers: merged });
449
+ } catch (error) {
450
+ const headers = new Headers({ "content-type": "application/json" });
451
+ applyNativeCors(req.headers, headers);
452
+ const jsonRpcId = await readJsonRpcId(req);
453
+ return new Response(
454
+ JSON.stringify({
455
+ jsonrpc: "2.0",
456
+ id: jsonRpcId,
457
+ error: {
458
+ code: -32603,
459
+ message: error instanceof Error ? error.message : "internal_error"
460
+ }
461
+ }),
462
+ { status: 500, headers }
463
+ );
464
+ } finally {
465
+ await transport.close().catch(() => {
466
+ });
467
+ releaseMutex();
468
+ }
469
+ };
470
+ }
471
+
472
+ // src/fetch/createSolvaPayMcpFetch.ts
473
+ function createSolvaPayMcpFetch(options) {
474
+ const {
475
+ // Descriptor options.
476
+ solvaPay,
477
+ productRef,
478
+ resourceUri,
479
+ htmlPath,
480
+ readHtml,
481
+ publicBaseUrl,
482
+ views,
483
+ csp,
484
+ getCustomerRef,
485
+ onToolCall,
486
+ onToolResult,
487
+ branding,
488
+ // Server / registration options.
489
+ additionalTools,
490
+ hideToolsByAudience,
491
+ registerPrompts = true,
492
+ registerDocsResources = true,
493
+ serverName,
494
+ serverVersion = "1.0.0",
495
+ // Handler options — everything in
496
+ // `CreateSolvaPayMcpFetchHandlerOptions` except `server`.
497
+ ...handlerRest
498
+ } = options;
499
+ const { server, descriptors } = buildSolvaPayMcpServer({
500
+ solvaPay,
501
+ productRef,
502
+ resourceUri,
503
+ ...htmlPath !== void 0 ? { htmlPath } : {},
504
+ ...readHtml !== void 0 ? { readHtml } : {},
505
+ publicBaseUrl,
506
+ ...views !== void 0 ? { views } : {},
507
+ ...csp !== void 0 ? { csp } : {},
508
+ ...getCustomerRef !== void 0 ? { getCustomerRef } : {},
509
+ ...onToolCall !== void 0 ? { onToolCall } : {},
510
+ ...onToolResult !== void 0 ? { onToolResult } : {},
511
+ ...branding !== void 0 ? { branding } : {},
512
+ registerPrompts,
513
+ registerDocsResources,
514
+ ...serverName !== void 0 ? { serverName } : {},
515
+ serverVersion
516
+ });
517
+ if (additionalTools) {
518
+ const registerPayable = (name, opts) => {
519
+ registerPayableTool(server, name, {
520
+ solvaPay,
521
+ ...opts,
522
+ product: opts.product ?? productRef,
523
+ buildBootstrap: opts.buildBootstrap ?? descriptors.buildBootstrapPayload
524
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
525
+ });
526
+ };
527
+ additionalTools({ server, solvaPay, resourceUri, productRef, registerPayable });
528
+ }
529
+ applyHideToolsByAudience(server, hideToolsByAudience);
530
+ return createSolvaPayMcpFetchHandler({
531
+ server,
532
+ publicBaseUrl,
533
+ productRef,
534
+ ...handlerRest
535
+ });
536
+ }
537
+
538
+ // src/fetch/index.ts
539
+ import {
540
+ getOAuthAuthorizationServerResponse as getOAuthAuthorizationServerResponse2,
541
+ getOAuthProtectedResourceResponse as getOAuthProtectedResourceResponse2,
542
+ buildAuthInfoFromBearer as buildAuthInfoFromBearer2,
543
+ McpBearerAuthError as McpBearerAuthError2
544
+ } from "@solvapay/mcp-core";
545
+ export {
546
+ McpBearerAuthError2 as McpBearerAuthError,
547
+ applyNativeCors,
548
+ authChallenge,
549
+ buildAuthInfoFromBearer2 as buildAuthInfoFromBearer,
550
+ corsPreflight,
551
+ createAuthorizationServerHandler,
552
+ createOAuthAuthorizeHandler,
553
+ createOAuthFetchRouter,
554
+ createOAuthRegisterHandler,
555
+ createOAuthRevokeHandler,
556
+ createOAuthTokenHandler,
557
+ createOpenidNotFoundHandler,
558
+ createProtectedResourceHandler,
559
+ createSolvaPayMcpFetch,
560
+ createSolvaPayMcpFetchHandler,
561
+ getOAuthAuthorizationServerResponse2 as getOAuthAuthorizationServerResponse,
562
+ getOAuthProtectedResourceResponse2 as getOAuthProtectedResourceResponse,
563
+ isNativeClientOrigin,
564
+ resolveBearer
565
+ };