@solvapay/mcp-core 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.
- package/README.md +33 -14
- package/dist/index.cjs +63 -78
- package/dist/index.d.cts +141 -126
- package/dist/index.d.ts +141 -126
- package/dist/index.js +62 -78
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
Framework-neutral MCP (Model Context Protocol) contracts for the SolvaPay SDK.
|
|
4
4
|
|
|
5
5
|
This package owns the shapes that cross the SolvaPay server ↔ client ↔
|
|
6
|
-
adapter boundary: tool names, descriptor builder
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
adapter boundary: tool names, descriptor builder (for SolvaPay intent
|
|
7
|
+
tools, which advertise `_meta.ui.resourceUri` on `tools/list` per
|
|
8
|
+
SEP-1865; merchant payable tools do NOT), the `BootstrapPayload`
|
|
9
|
+
shape carried on `structuredContent`, Stripe CSP baseline, pure OAuth
|
|
10
|
+
discovery JSON builders, and JWT bearer helpers.
|
|
9
11
|
|
|
10
12
|
It does **not** depend on `@modelcontextprotocol/sdk`,
|
|
11
13
|
`@modelcontextprotocol/ext-apps`, or any runtime-specific HTTP plumbing.
|
|
12
14
|
The official `@modelcontextprotocol/*` adapter lives in
|
|
13
|
-
[`@solvapay/mcp`](../mcp); Node `(req, res, next)` OAuth middleware
|
|
14
|
-
|
|
15
|
-
turnkey handler
|
|
15
|
+
[`@solvapay/mcp`](../mcp); Node `(req, res, next)` OAuth middleware ships
|
|
16
|
+
as the [`@solvapay/mcp/express`](../mcp/src/express) subpath export;
|
|
17
|
+
fetch-first OAuth + the turnkey handler ships as the
|
|
18
|
+
[`@solvapay/mcp/fetch`](../mcp/src/fetch) subpath export.
|
|
16
19
|
|
|
17
20
|
## Install
|
|
18
21
|
|
|
@@ -26,14 +29,30 @@ pnpm add @solvapay/mcp-core @solvapay/server
|
|
|
26
29
|
|---|---|
|
|
27
30
|
| `MCP_TOOL_NAMES`, `McpToolName` | You're implementing a SolvaPay MCP transport tool on any framework and need the canonical tool names |
|
|
28
31
|
| `buildSolvaPayDescriptors(opts)` | You're writing an MCP adapter (`fastmcp`, raw JSON-RPC) and want the full SolvaPay tool surface as descriptor objects |
|
|
29
|
-
| `buildPayableHandler(solvaPay, ctx, handler)` | You're hand-rolling a paywall-protected tool and
|
|
30
|
-
| `paywallToolResult(errOrGate
|
|
31
|
-
| `buildPaywallUiMeta({ resourceUri, toolName })` | You're building the `_meta.ui` envelope yourself |
|
|
32
|
+
| `buildPayableHandler(solvaPay, ctx, handler)` | You're hand-rolling a paywall-protected tool and want the pre-check paywall to return a clean text-only narration on `content[0].text` + the gate on `structuredContent` |
|
|
33
|
+
| `paywallToolResult(errOrGate)` | You have a `PaywallError` (legacy `try/catch`) or a `PaywallStructuredContent` gate from `paywall.decide()` and want a text-only tool result |
|
|
32
34
|
| `SOLVAPAY_DEFAULT_CSP`, `mergeCsp(overrides)` | You're registering the SolvaPay UI resource and want the Stripe allow-list baked in |
|
|
33
35
|
| `getOAuthAuthorizationServerResponse(opts)`, `getOAuthProtectedResourceResponse(url)` | You're serving the `.well-known/*` discovery JSON from any runtime |
|
|
34
36
|
| `buildAuthInfoFromBearer(header, opts)` | You're plugging a raw `Authorization: Bearer …` header into an MCP `authInfo` envelope |
|
|
35
37
|
| `McpBearerAuthError`, `extractBearerToken`, `decodeJwtPayload`, `getCustomerRefFromJwtPayload`, `getCustomerRefFromBearerAuthHeader` | Low-level JWT bearer parsing — no signature verification (validate upstream first) |
|
|
36
38
|
|
|
39
|
+
## How paywalls work
|
|
40
|
+
|
|
41
|
+
Paywall responses from `buildPayableHandler` / `paywallToolResult` are
|
|
42
|
+
**text-only**. `content[0].text` carries a human narration that names
|
|
43
|
+
the recovery intent tool (`upgrade` / `topup` / `activate_plan`) and
|
|
44
|
+
inlines `gate.checkoutUrl` for terminal-first hosts. `isError` is
|
|
45
|
+
always `false` — a gate is a user-actionable signal, not a tool
|
|
46
|
+
failure. `structuredContent = gate` is still emitted for programmatic
|
|
47
|
+
consumers.
|
|
48
|
+
|
|
49
|
+
The widget iframe is reserved for the three deliberate intent tools
|
|
50
|
+
(`upgrade` / `manage_account` / `topup`), which advertise
|
|
51
|
+
`_meta.ui.resourceUri` on their descriptors. Merchant payable tools
|
|
52
|
+
don't, so hosts don't open an uninvited iframe on a successful data
|
|
53
|
+
call or on a paywall — the LLM narrates the recovery and calls the
|
|
54
|
+
intent tool, which mounts the widget.
|
|
55
|
+
|
|
37
56
|
## Typical usage
|
|
38
57
|
|
|
39
58
|
If you're starting a new SolvaPay MCP server on the official
|
|
@@ -41,9 +60,9 @@ If you're starting a new SolvaPay MCP server on the official
|
|
|
41
60
|
[`@solvapay/mcp`](../mcp), which wraps it with one-call ergonomics.
|
|
42
61
|
|
|
43
62
|
If you need HTTP-level OAuth handlers, pair this package with either
|
|
44
|
-
[`@solvapay/mcp
|
|
45
|
-
[`@solvapay/mcp
|
|
46
|
-
Workers / Bun / Next edge).
|
|
63
|
+
[`@solvapay/mcp/express`](../mcp/src/express) (Node) or
|
|
64
|
+
[`@solvapay/mcp/fetch`](../mcp/src/fetch) (Deno / Supabase Edge /
|
|
65
|
+
Cloudflare Workers / Bun / Next edge).
|
|
47
66
|
|
|
48
67
|
If you're writing a new adapter (fastmcp, custom JSON-RPC):
|
|
49
68
|
|
|
@@ -74,7 +93,7 @@ myAdapter.registerResource(resource)
|
|
|
74
93
|
## See also
|
|
75
94
|
|
|
76
95
|
- [`@solvapay/mcp`](../mcp) — official `@modelcontextprotocol/sdk` + `ext-apps` adapter (`createSolvaPayMcpServer`)
|
|
77
|
-
- [`@solvapay/mcp
|
|
78
|
-
- [`@solvapay/mcp
|
|
96
|
+
- [`@solvapay/mcp/express`](../mcp/src/express) — Node `(req, res, next)` OAuth middleware stack
|
|
97
|
+
- [`@solvapay/mcp/fetch`](../mcp/src/fetch) — fetch-first OAuth handlers + turnkey `createSolvaPayMcpFetchHandler` / `createSolvaPayMcpFetch`
|
|
79
98
|
- [`@solvapay/server`](../server) — core SDK (paywall, webhooks, `*Core` helpers)
|
|
80
99
|
- [`@solvapay/react/mcp`](../react) — React provider + views for the MCP App UI shell
|
package/dist/index.cjs
CHANGED
|
@@ -43,16 +43,17 @@ __export(index_exports, {
|
|
|
43
43
|
TOOL_FOR_VIEW: () => TOOL_FOR_VIEW,
|
|
44
44
|
VIEW_FOR_OPEN_TOOL: () => VIEW_FOR_OPEN_TOOL,
|
|
45
45
|
VIEW_FOR_TOOL: () => VIEW_FOR_TOOL,
|
|
46
|
+
applyHideToolsByAudience: () => applyHideToolsByAudience,
|
|
46
47
|
balanceSummary: () => balanceSummary,
|
|
47
48
|
buildAuthInfoFromBearer: () => buildAuthInfoFromBearer,
|
|
48
49
|
buildPayableHandler: () => buildPayableHandler,
|
|
49
|
-
buildPaywallUiMeta: () => buildPaywallUiMeta,
|
|
50
50
|
buildSolvaPayDescriptors: () => buildSolvaPayDescriptors,
|
|
51
51
|
buildSolvaPayPrompts: () => buildSolvaPayPrompts,
|
|
52
52
|
buildSolvaPayRequest: () => buildSolvaPayRequest,
|
|
53
53
|
createBuildBootstrapPayload: () => createBuildBootstrapPayload,
|
|
54
54
|
decodeJwtPayload: () => decodeJwtPayload,
|
|
55
55
|
defaultGetCustomerRef: () => defaultGetCustomerRef,
|
|
56
|
+
deriveIcons: () => deriveIcons,
|
|
56
57
|
enrichPurchase: () => enrichPurchase,
|
|
57
58
|
extractBearerToken: () => extractBearerToken,
|
|
58
59
|
getCustomerRefFromBearerAuthHeader: () => getCustomerRefFromBearerAuthHeader,
|
|
@@ -95,9 +96,7 @@ var MCP_TOOL_NAMES = {
|
|
|
95
96
|
var SOLVAPAY_MCP_VIEW_KINDS = [
|
|
96
97
|
"checkout",
|
|
97
98
|
"account",
|
|
98
|
-
"topup"
|
|
99
|
-
"paywall",
|
|
100
|
-
"nudge"
|
|
99
|
+
"topup"
|
|
101
100
|
];
|
|
102
101
|
var TOOL_FOR_VIEW = {
|
|
103
102
|
checkout: "upgrade",
|
|
@@ -443,41 +442,19 @@ function previewJson(value, max = 400) {
|
|
|
443
442
|
}
|
|
444
443
|
}
|
|
445
444
|
|
|
446
|
-
// src/paywall-meta.ts
|
|
447
|
-
function buildPaywallUiMeta(input) {
|
|
448
|
-
return {
|
|
449
|
-
ui: {
|
|
450
|
-
resourceUri: input.resourceUri
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
445
|
// src/paywallToolResult.ts
|
|
456
446
|
var import_server = require("@solvapay/server");
|
|
457
|
-
async function paywallToolResult(errOrGate,
|
|
447
|
+
async function paywallToolResult(errOrGate, _ctx = {}) {
|
|
458
448
|
const paywallContent = errOrGate instanceof import_server.PaywallError ? errOrGate.structuredContent : errOrGate;
|
|
459
449
|
const narrationText = errOrGate instanceof import_server.PaywallError ? errOrGate.message : paywallContent.message;
|
|
460
|
-
let structuredContent;
|
|
461
|
-
if (ctx.buildBootstrap) {
|
|
462
|
-
const bootstrap = await ctx.buildBootstrap("paywall", ctx.extra, {
|
|
463
|
-
paywall: paywallContent
|
|
464
|
-
});
|
|
465
|
-
structuredContent = bootstrap;
|
|
466
|
-
} else {
|
|
467
|
-
structuredContent = paywallContent;
|
|
468
|
-
}
|
|
469
450
|
return {
|
|
470
451
|
// Deliberately `false`: paywall is a user-actionable gate, not a
|
|
471
|
-
// tool failure.
|
|
472
|
-
//
|
|
473
|
-
// `
|
|
474
|
-
// MCPJam / Claude Desktop / ChatGPT Apps. The `structuredContent`
|
|
475
|
-
// and `content[0].text` still carry the gate reason so the LLM
|
|
476
|
-
// can narrate it, while `_meta.ui` triggers the widget.
|
|
452
|
+
// tool failure. The LLM narrates the recovery from
|
|
453
|
+
// `content[0].text` and the structured gate content on
|
|
454
|
+
// `structuredContent` is available for programmatic consumers.
|
|
477
455
|
isError: false,
|
|
478
456
|
content: [{ type: "text", text: narrationText }],
|
|
479
|
-
structuredContent
|
|
480
|
-
_meta: buildPaywallUiMeta({ resourceUri: ctx.resourceUri })
|
|
457
|
+
structuredContent: paywallContent
|
|
481
458
|
};
|
|
482
459
|
}
|
|
483
460
|
|
|
@@ -507,6 +484,30 @@ function mergeCsp(overrides) {
|
|
|
507
484
|
};
|
|
508
485
|
}
|
|
509
486
|
|
|
487
|
+
// src/hideToolsByAudience.ts
|
|
488
|
+
function applyHideToolsByAudience(server, audiences) {
|
|
489
|
+
if (!audiences || audiences.length === 0) return;
|
|
490
|
+
const hidden = new Set(audiences);
|
|
491
|
+
const inner = server.server;
|
|
492
|
+
if (!inner || typeof inner !== "object" || !(inner._requestHandlers instanceof Map)) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const handlers = inner._requestHandlers;
|
|
496
|
+
const original = handlers.get("tools/list");
|
|
497
|
+
if (!original) return;
|
|
498
|
+
handlers.set("tools/list", async (req, extra) => {
|
|
499
|
+
const res = await original(req, extra);
|
|
500
|
+
const tools = Array.isArray(res?.tools) ? res.tools : [];
|
|
501
|
+
return {
|
|
502
|
+
...res,
|
|
503
|
+
tools: tools.filter((t) => {
|
|
504
|
+
const audience = t?._meta?.audience;
|
|
505
|
+
return !hidden.has(typeof audience === "string" ? audience : "");
|
|
506
|
+
})
|
|
507
|
+
};
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
510
511
|
// src/descriptors.ts
|
|
511
512
|
var import_server3 = require("@solvapay/server");
|
|
512
513
|
var import_zod = require("zod");
|
|
@@ -531,7 +532,7 @@ function createBuildBootstrapPayload(options) {
|
|
|
531
532
|
};
|
|
532
533
|
const buildRequest = (extra) => buildSolvaPayRequest(extra, { getCustomerRef });
|
|
533
534
|
const productQueryRequest = () => buildSolvaPayRequest(void 0, { query: { productRef }, getCustomerRef: () => null });
|
|
534
|
-
return async (view, extra
|
|
535
|
+
return async (view, extra) => {
|
|
535
536
|
const customerRef = getCustomerRef(extra);
|
|
536
537
|
const wrapError = (promise) => promise.catch((err) => ({
|
|
537
538
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -578,7 +579,7 @@ function createBuildBootstrapPayload(options) {
|
|
|
578
579
|
balance: okOrNull(balanceResult),
|
|
579
580
|
usage: okOrNull(usageResult)
|
|
580
581
|
} : null;
|
|
581
|
-
|
|
582
|
+
return {
|
|
582
583
|
view,
|
|
583
584
|
productRef,
|
|
584
585
|
stripePublishableKey,
|
|
@@ -588,8 +589,6 @@ function createBuildBootstrapPayload(options) {
|
|
|
588
589
|
plans,
|
|
589
590
|
customer
|
|
590
591
|
};
|
|
591
|
-
if (extras.paywall) payload.paywall = extras.paywall;
|
|
592
|
-
return payload;
|
|
593
592
|
};
|
|
594
593
|
}
|
|
595
594
|
|
|
@@ -623,9 +622,13 @@ with clickable URLs for text-only hosts.
|
|
|
623
622
|
|
|
624
623
|
Each intent tool returns a full \`BootstrapPayload\` (merchant + product + plans
|
|
625
624
|
+ customer snapshot) so the embedded UI never fires per-view read calls. When
|
|
626
|
-
a paywalled data tool hits the usage limit, its response
|
|
627
|
-
|
|
628
|
-
|
|
625
|
+
a paywalled data tool hits the usage limit, its response is plain text: the
|
|
626
|
+
\`content[0].text\` narration names the recovery intent tool
|
|
627
|
+
(\`upgrade\` / \`topup\` / \`activate_plan\`) and inlines \`gate.checkoutUrl\` for
|
|
628
|
+
terminal-first hosts. The structured gate rides on \`structuredContent\` for
|
|
629
|
+
programmatic consumers. No widget iframe opens for a gate \u2014 the LLM reads the
|
|
630
|
+
narration and, if the user agrees, calls the named intent tool which mounts
|
|
631
|
+
the checkout / topup / account surface.
|
|
629
632
|
|
|
630
633
|
Auth is handled by the SolvaPay OAuth bridge (see \`createMcpOAuthBridge\`). The
|
|
631
634
|
bridge injects \`customer_ref\` onto every authenticated request; tools that
|
|
@@ -1138,7 +1141,7 @@ function buildResponseContext(params) {
|
|
|
1138
1141
|
|
|
1139
1142
|
// src/payable-handler.ts
|
|
1140
1143
|
function buildPayableHandler(solvaPay, ctx, handler) {
|
|
1141
|
-
const { product,
|
|
1144
|
+
const { product, getCustomerRef } = ctx;
|
|
1142
1145
|
const wrappedBusinessLogic = async (args, handlerContext) => {
|
|
1143
1146
|
const limits = handlerContext?.limits ?? null;
|
|
1144
1147
|
const customerRef = handlerContext?.customerRef ?? "";
|
|
@@ -1154,60 +1157,44 @@ function buildPayableHandler(solvaPay, ctx, handler) {
|
|
|
1154
1157
|
return async (args, extra) => {
|
|
1155
1158
|
const result = await protectedHandler(args, extra);
|
|
1156
1159
|
if ((0, import_server5.isPaywallStructuredContent)(result.structuredContent)) {
|
|
1157
|
-
const existingMeta = typeof result._meta === "object" && result._meta !== null ? result._meta : {};
|
|
1158
|
-
const gateContent = result.structuredContent;
|
|
1159
|
-
const structuredContent = buildBootstrap ? await buildBootstrap("paywall", extra, {
|
|
1160
|
-
paywall: gateContent
|
|
1161
|
-
}) : result.structuredContent;
|
|
1162
1160
|
return {
|
|
1163
1161
|
...result,
|
|
1164
1162
|
isError: false,
|
|
1165
|
-
structuredContent
|
|
1166
|
-
_meta: {
|
|
1167
|
-
...existingMeta,
|
|
1168
|
-
...buildPaywallUiMeta({ resourceUri })
|
|
1169
|
-
}
|
|
1163
|
+
structuredContent: result.structuredContent
|
|
1170
1164
|
};
|
|
1171
1165
|
}
|
|
1172
1166
|
if (result.isError) {
|
|
1173
1167
|
return result;
|
|
1174
1168
|
}
|
|
1175
1169
|
const envelope = assertResponseResult(result.structuredContent);
|
|
1176
|
-
return unwrapResponseEnvelope(
|
|
1177
|
-
result,
|
|
1178
|
-
envelope,
|
|
1179
|
-
resourceUri,
|
|
1180
|
-
buildBootstrap,
|
|
1181
|
-
extra
|
|
1182
|
-
);
|
|
1170
|
+
return unwrapResponseEnvelope(result, envelope, extra);
|
|
1183
1171
|
};
|
|
1184
1172
|
}
|
|
1185
|
-
async function unwrapResponseEnvelope(adapterResult, envelope,
|
|
1173
|
+
async function unwrapResponseEnvelope(adapterResult, envelope, _extra) {
|
|
1186
1174
|
const { data, options, emittedBlocks } = envelope;
|
|
1187
1175
|
const textOverride = options?.text;
|
|
1188
1176
|
const nudge = options?.nudge;
|
|
1189
|
-
const
|
|
1177
|
+
const baseText = typeof textOverride === "string" ? textOverride : JSON.stringify(data);
|
|
1178
|
+
let primaryText = baseText;
|
|
1179
|
+
if (nudge) {
|
|
1180
|
+
const nudgeText = nudge.message && nudge.message.length > 0 ? nudge.message : (0, import_server5.buildNudgeMessage)(
|
|
1181
|
+
// `buildNudgeMessage` only reads the state kind to pick
|
|
1182
|
+
// copy; for merchant-supplied nudges we don't have a
|
|
1183
|
+
// `LimitResponseWithPlan` in hand here, so we defer to
|
|
1184
|
+
// the nudge's own kind → state mapping. `low-balance` →
|
|
1185
|
+
// topup, everything else → upgrade.
|
|
1186
|
+
nudge.kind === "low-balance" ? { kind: "topup_required" } : { kind: "upgrade_required" },
|
|
1187
|
+
null
|
|
1188
|
+
);
|
|
1189
|
+
primaryText = baseText.length > 0 ? `${baseText}
|
|
1190
|
+
|
|
1191
|
+
${nudgeText}` : nudgeText;
|
|
1192
|
+
}
|
|
1190
1193
|
const content = [
|
|
1191
1194
|
...emittedBlocks ?? [],
|
|
1192
1195
|
{ type: "text", text: primaryText }
|
|
1193
1196
|
];
|
|
1194
1197
|
const existingMeta = typeof adapterResult._meta === "object" && adapterResult._meta !== null ? adapterResult._meta : {};
|
|
1195
|
-
if (nudge) {
|
|
1196
|
-
const structuredContent = buildBootstrap ? {
|
|
1197
|
-
...await buildBootstrap("nudge", extra),
|
|
1198
|
-
nudge,
|
|
1199
|
-
data
|
|
1200
|
-
} : data;
|
|
1201
|
-
return {
|
|
1202
|
-
...adapterResult,
|
|
1203
|
-
content,
|
|
1204
|
-
structuredContent,
|
|
1205
|
-
_meta: {
|
|
1206
|
-
...existingMeta,
|
|
1207
|
-
...buildNudgeUiMeta({ resourceUri, nudge })
|
|
1208
|
-
}
|
|
1209
|
-
};
|
|
1210
|
-
}
|
|
1211
1198
|
return {
|
|
1212
1199
|
...adapterResult,
|
|
1213
1200
|
content,
|
|
@@ -1215,9 +1202,6 @@ async function unwrapResponseEnvelope(adapterResult, envelope, resourceUri, buil
|
|
|
1215
1202
|
...Object.keys(existingMeta).length > 0 ? { _meta: existingMeta } : {}
|
|
1216
1203
|
};
|
|
1217
1204
|
}
|
|
1218
|
-
function buildNudgeUiMeta(input) {
|
|
1219
|
-
return { ui: { resourceUri: input.resourceUri, nudge: input.nudge } };
|
|
1220
|
-
}
|
|
1221
1205
|
|
|
1222
1206
|
// src/oauth-discovery.ts
|
|
1223
1207
|
var DEFAULT_OAUTH_PATHS = {
|
|
@@ -1364,16 +1348,17 @@ function buildAuthInfoFromBearer(authorization, options = {}) {
|
|
|
1364
1348
|
TOOL_FOR_VIEW,
|
|
1365
1349
|
VIEW_FOR_OPEN_TOOL,
|
|
1366
1350
|
VIEW_FOR_TOOL,
|
|
1351
|
+
applyHideToolsByAudience,
|
|
1367
1352
|
balanceSummary,
|
|
1368
1353
|
buildAuthInfoFromBearer,
|
|
1369
1354
|
buildPayableHandler,
|
|
1370
|
-
buildPaywallUiMeta,
|
|
1371
1355
|
buildSolvaPayDescriptors,
|
|
1372
1356
|
buildSolvaPayPrompts,
|
|
1373
1357
|
buildSolvaPayRequest,
|
|
1374
1358
|
createBuildBootstrapPayload,
|
|
1375
1359
|
decodeJwtPayload,
|
|
1376
1360
|
defaultGetCustomerRef,
|
|
1361
|
+
deriveIcons,
|
|
1377
1362
|
enrichPurchase,
|
|
1378
1363
|
extractBearerToken,
|
|
1379
1364
|
getCustomerRefFromBearerAuthHeader,
|