@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.
- package/README.md +35 -9
- package/dist/chunk-3VDAADZU.js +211 -0
- package/dist/express/index.cjs +504 -0
- package/dist/express/index.d.cts +65 -0
- package/dist/express/index.d.ts +65 -0
- package/dist/express/index.js +481 -0
- package/dist/fetch/index.cjs +787 -0
- package/dist/fetch/index.d.cts +238 -0
- package/dist/fetch/index.d.ts +238 -0
- package/dist/fetch/index.js +565 -0
- package/dist/index.cjs +116 -75
- package/dist/index.d.cts +46 -30
- package/dist/index.d.ts +46 -30
- package/dist/index.js +14 -171
- package/package.json +16 -7
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ Framework-neutral contracts (tool names, descriptors, paywall meta,
|
|
|
8
8
|
OAuth discovery JSON, JWT helpers) live in
|
|
9
9
|
[`@solvapay/mcp-core`](../mcp-core) so alternative adapters
|
|
10
10
|
(`fastmcp`, raw JSON-RPC) can reuse the same contract. Runtime-specific
|
|
11
|
-
OAuth middleware
|
|
11
|
+
OAuth middleware ships as two subpath exports of this package:
|
|
12
12
|
|
|
13
|
-
- [`@solvapay/mcp
|
|
14
|
-
- [`@solvapay/mcp
|
|
13
|
+
- [`@solvapay/mcp/express`](./src/express) — Node `(req, res, next)`.
|
|
14
|
+
- [`@solvapay/mcp/fetch`](./src/fetch) — Web standards `(req: Request) => Promise<Response>` + the turnkey `createSolvaPayMcpFetch` factory for Deno / Supabase Edge / Cloudflare Workers / Bun / Next edge / Vercel Functions.
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
@@ -78,10 +78,16 @@ branded envelope produced by `ctx.respond(data, options?)`.
|
|
|
78
78
|
```ts
|
|
79
79
|
handler: async ({ prompt }, ctx) => {
|
|
80
80
|
const video = await generate(prompt)
|
|
81
|
-
//
|
|
81
|
+
// Append a text-only nudge when the customer is low on credits.
|
|
82
|
+
// The nudge message is appended to `content[0].text` as a plain
|
|
83
|
+
// suffix — no widget iframe, no `structuredContent` switch. Point
|
|
84
|
+
// the user at the recovery intent tool by name.
|
|
82
85
|
if (ctx.customer.balance < 500) {
|
|
83
86
|
return ctx.respond({ videoUrl: video.url }, {
|
|
84
|
-
nudge: {
|
|
87
|
+
nudge: {
|
|
88
|
+
kind: 'low-balance',
|
|
89
|
+
message: 'Running low on credits — call the `topup` tool to add more.',
|
|
90
|
+
},
|
|
85
91
|
})
|
|
86
92
|
}
|
|
87
93
|
return ctx.respond({ videoUrl: video.url })
|
|
@@ -95,9 +101,10 @@ full surface. The TL;DR:
|
|
|
95
101
|
`balance` / `remaining` / `plan` to branch on usage; call
|
|
96
102
|
`ctx.customer.fresh()` for a fresh fetch when staleness matters.
|
|
97
103
|
- `ctx.respond(data, options?)` — returns a branded envelope. `options`
|
|
98
|
-
carries `text` (override `content[0].text`), `nudge` (
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
carries `text` (override `content[0].text`), `nudge` (the nudge
|
|
105
|
+
message is appended to `content[0].text` as a plain-text suffix —
|
|
106
|
+
no widget surface, no `structuredContent` switch), and the reserved
|
|
107
|
+
`units` (V1.1 variable-unit billing — V1 silently ignores).
|
|
101
108
|
- `ctx.gate(reason?)` — stops handler execution and routes a paywall
|
|
102
109
|
response through the adapter's `formatGate` channel. Rare — the
|
|
103
110
|
SDK normally fires the paywall automatically via `payable().mcp()`
|
|
@@ -106,12 +113,31 @@ full surface. The TL;DR:
|
|
|
106
113
|
surface. V1 queues (emit) or no-ops (progress / signal); V1.1 wires
|
|
107
114
|
them to SSE and transport cancellation without code changes.
|
|
108
115
|
|
|
116
|
+
## How paywalls work
|
|
117
|
+
|
|
118
|
+
Paywall responses from `registerPayable` tools are **text-only**:
|
|
119
|
+
|
|
120
|
+
- `isError: false`, so hosts don't short-circuit on the error path.
|
|
121
|
+
- `structuredContent = gate` for programmatic consumers.
|
|
122
|
+
- `content[0].text` carries a state-engine-generated narration that
|
|
123
|
+
names the recovery intent tool (`upgrade` for no active plan,
|
|
124
|
+
`topup` for usage-based zero-balance, `activate_plan` for pending
|
|
125
|
+
activation) and inlines `gate.checkoutUrl` for terminal-first
|
|
126
|
+
hosts (Claude Code, CLI MCP clients).
|
|
127
|
+
|
|
128
|
+
The LLM reads that narration, tells the user, and either (a) the
|
|
129
|
+
user clicks the inline URL and completes checkout in the browser, or
|
|
130
|
+
(b) the LLM calls the named intent tool which mounts the SolvaPay
|
|
131
|
+
widget on `McpCheckoutView` / `McpTopupView`. Only those three
|
|
132
|
+
intent tools advertise `_meta.ui.resourceUri` — merchant payable
|
|
133
|
+
tools don't, so no uninvited iframe opens on a silent success.
|
|
134
|
+
|
|
109
135
|
## What's in the box
|
|
110
136
|
|
|
111
137
|
| Export | Use when |
|
|
112
138
|
|---|---|
|
|
113
139
|
| `createSolvaPayMcpServer(opts)` | You want the batteries-included `McpServer` with every SolvaPay tool registered |
|
|
114
|
-
| `registerPayableTool(server, name, opts)` | You want to add a paywall-protected tool to an existing `McpServer`.
|
|
140
|
+
| `registerPayableTool(server, name, opts)` | You want to add a paywall-protected tool to an existing `McpServer`. Paywall / nudge responses are text-only narrations; the widget iframe stays reserved for the three intent tools. |
|
|
115
141
|
|
|
116
142
|
## Peer dependencies
|
|
117
143
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// src/registerPayableTool.ts
|
|
2
|
+
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
|
+
import {
|
|
4
|
+
buildPayableHandler
|
|
5
|
+
} from "@solvapay/mcp-core";
|
|
6
|
+
function registerPayableTool(server, name, options) {
|
|
7
|
+
const {
|
|
8
|
+
solvaPay,
|
|
9
|
+
schema,
|
|
10
|
+
product,
|
|
11
|
+
title,
|
|
12
|
+
description,
|
|
13
|
+
handler,
|
|
14
|
+
buildBootstrap,
|
|
15
|
+
getCustomerRef,
|
|
16
|
+
meta,
|
|
17
|
+
annotations,
|
|
18
|
+
icons
|
|
19
|
+
} = options;
|
|
20
|
+
const protectedHandler = buildPayableHandler(
|
|
21
|
+
solvaPay,
|
|
22
|
+
{ product, buildBootstrap, getCustomerRef },
|
|
23
|
+
handler
|
|
24
|
+
);
|
|
25
|
+
const baseMeta = meta ?? {};
|
|
26
|
+
const baseUi = baseMeta.ui ?? {};
|
|
27
|
+
const hasIcons = icons !== void 0 && icons.length > 0;
|
|
28
|
+
const mergedUi = {
|
|
29
|
+
...baseUi,
|
|
30
|
+
...hasIcons ? { icons } : {}
|
|
31
|
+
};
|
|
32
|
+
const hasUi = Object.keys(mergedUi).length > 0;
|
|
33
|
+
const toolMeta = hasUi ? { ...baseMeta, ui: mergedUi } : { ...baseMeta };
|
|
34
|
+
const effectiveAnnotations = {
|
|
35
|
+
readOnlyHint: true,
|
|
36
|
+
openWorldHint: true,
|
|
37
|
+
...annotations
|
|
38
|
+
};
|
|
39
|
+
const hasUiResource = hasUi && typeof mergedUi.resourceUri === "string";
|
|
40
|
+
const toolConfig = {
|
|
41
|
+
...title !== void 0 ? { title } : {},
|
|
42
|
+
...description !== void 0 ? { description } : {},
|
|
43
|
+
...schema !== void 0 ? { inputSchema: schema } : {},
|
|
44
|
+
...Object.keys(toolMeta).length > 0 ? { _meta: toolMeta } : {},
|
|
45
|
+
annotations: effectiveAnnotations,
|
|
46
|
+
...icons !== void 0 && icons.length > 0 ? { icons } : {}
|
|
47
|
+
};
|
|
48
|
+
const toolCallback = async (args, extra) => await protectedHandler(args, extra);
|
|
49
|
+
if (hasUiResource) {
|
|
50
|
+
return registerAppTool(
|
|
51
|
+
server,
|
|
52
|
+
name,
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
toolConfig,
|
|
55
|
+
toolCallback
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return server.registerTool(
|
|
59
|
+
name,
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
toolConfig,
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
toolCallback
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/internal/buildMcpServer.ts
|
|
68
|
+
import {
|
|
69
|
+
registerAppResource,
|
|
70
|
+
registerAppTool as registerAppTool2,
|
|
71
|
+
RESOURCE_MIME_TYPE
|
|
72
|
+
} from "@modelcontextprotocol/ext-apps/server";
|
|
73
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
74
|
+
import {
|
|
75
|
+
applyHideToolsByAudience,
|
|
76
|
+
buildSolvaPayDescriptors,
|
|
77
|
+
deriveIcons
|
|
78
|
+
} from "@solvapay/mcp-core";
|
|
79
|
+
function registerDescriptor(server, tool) {
|
|
80
|
+
const baseMeta = tool.meta ?? {};
|
|
81
|
+
const baseUi = baseMeta.ui ?? {};
|
|
82
|
+
const metaWithIcons = tool.icons && tool.icons.length > 0 ? { ...baseMeta, ui: { ...baseUi, icons: tool.icons } } : baseMeta;
|
|
83
|
+
registerAppTool2(
|
|
84
|
+
server,
|
|
85
|
+
tool.name,
|
|
86
|
+
{
|
|
87
|
+
...tool.title !== void 0 ? { title: tool.title } : {},
|
|
88
|
+
description: tool.description,
|
|
89
|
+
inputSchema: tool.inputSchema,
|
|
90
|
+
_meta: metaWithIcons,
|
|
91
|
+
...tool.annotations !== void 0 ? { annotations: tool.annotations } : {},
|
|
92
|
+
...tool.icons !== void 0 ? { icons: tool.icons } : {}
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
+
},
|
|
95
|
+
// `SolvaPayCallToolResult` is a structural subset of the official
|
|
96
|
+
// SDK's `CallToolResult`; cast to erase the extra-narrow `resource`
|
|
97
|
+
// block typing the SDK expects on `{ type: 'resource' }` content.
|
|
98
|
+
async (args, extra) => await tool.handler(
|
|
99
|
+
args,
|
|
100
|
+
extra
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
function registerPromptDescriptor(server, prompt) {
|
|
105
|
+
const config = { description: prompt.description };
|
|
106
|
+
if (prompt.title !== void 0) config.title = prompt.title;
|
|
107
|
+
if (prompt.argsSchema !== void 0) config.argsSchema = prompt.argsSchema;
|
|
108
|
+
server.registerPrompt(
|
|
109
|
+
prompt.name,
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
config,
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
async (args) => await prompt.handler(args ?? {})
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
function registerDocsResource(server, docs) {
|
|
117
|
+
server.registerResource(
|
|
118
|
+
docs.name,
|
|
119
|
+
docs.uri,
|
|
120
|
+
{
|
|
121
|
+
...docs.title !== void 0 ? { title: docs.title } : {},
|
|
122
|
+
description: docs.description,
|
|
123
|
+
mimeType: docs.mimeType
|
|
124
|
+
},
|
|
125
|
+
async () => ({
|
|
126
|
+
contents: [
|
|
127
|
+
{
|
|
128
|
+
uri: docs.uri,
|
|
129
|
+
mimeType: docs.mimeType,
|
|
130
|
+
text: await docs.readBody()
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
function buildSolvaPayMcpServer(options) {
|
|
137
|
+
const {
|
|
138
|
+
registerPrompts = true,
|
|
139
|
+
registerDocsResources = true,
|
|
140
|
+
serverName,
|
|
141
|
+
serverVersion = "1.0.0",
|
|
142
|
+
hideToolsByAudience: _hideToolsByAudience,
|
|
143
|
+
...descriptorOptions
|
|
144
|
+
} = options;
|
|
145
|
+
const descriptors = buildSolvaPayDescriptors(descriptorOptions);
|
|
146
|
+
const effectiveServerName = serverName ?? descriptorOptions.branding?.brandName ?? "solvapay-mcp-server";
|
|
147
|
+
const serverIcons = deriveIcons(descriptorOptions.branding);
|
|
148
|
+
const server = new McpServer({
|
|
149
|
+
name: effectiveServerName,
|
|
150
|
+
version: serverVersion,
|
|
151
|
+
...serverIcons ? { icons: serverIcons } : {}
|
|
152
|
+
});
|
|
153
|
+
for (const tool of descriptors.tools) {
|
|
154
|
+
registerDescriptor(server, tool);
|
|
155
|
+
}
|
|
156
|
+
if (registerPrompts) {
|
|
157
|
+
for (const prompt of descriptors.prompts) {
|
|
158
|
+
registerPromptDescriptor(server, prompt);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (registerDocsResources) {
|
|
162
|
+
for (const docs of descriptors.docsResources) {
|
|
163
|
+
registerDocsResource(server, docs);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const resource = descriptors.resource;
|
|
167
|
+
registerAppResource(
|
|
168
|
+
server,
|
|
169
|
+
resource.uri,
|
|
170
|
+
resource.uri,
|
|
171
|
+
{
|
|
172
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
173
|
+
_meta: {
|
|
174
|
+
ui: {
|
|
175
|
+
csp: resource.csp,
|
|
176
|
+
// `false` asks the host to skip painting its own outer card /
|
|
177
|
+
// border around the iframe. The widget paints its own frame
|
|
178
|
+
// via `.solvapay-mcp-card`, and `<AppHeader>` renders the
|
|
179
|
+
// merchant mark at the top; a host-painted card on top of
|
|
180
|
+
// that produced a nested-container look (visible on MCP Jam
|
|
181
|
+
// with the earlier `true` default). Hosts that honour the
|
|
182
|
+
// preference (per the MCP Apps spec) now render us flush
|
|
183
|
+
// inside their conversation surface.
|
|
184
|
+
prefersBorder: false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
async () => ({
|
|
189
|
+
contents: [
|
|
190
|
+
{
|
|
191
|
+
uri: resource.uri,
|
|
192
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
193
|
+
text: await resource.readHtml(),
|
|
194
|
+
_meta: {
|
|
195
|
+
ui: {
|
|
196
|
+
csp: resource.csp,
|
|
197
|
+
prefersBorder: false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
return { server, descriptors };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export {
|
|
208
|
+
applyHideToolsByAudience,
|
|
209
|
+
buildSolvaPayMcpServer,
|
|
210
|
+
registerPayableTool
|
|
211
|
+
};
|