solvapay 0.1.0.dev4__py3-none-any.whl
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.
- solvapay/__init__.py +26 -0
- solvapay/_currencies.py +170 -0
- solvapay/_generated/__init__.py +1 -0
- solvapay/_generated/models.py +3643 -0
- solvapay/_generated/operations.py +1894 -0
- solvapay/_http.py +243 -0
- solvapay/_js.py +114 -0
- solvapay/_tax_ids.py +442 -0
- solvapay/agents/__init__.py +67 -0
- solvapay/agents/calls.py +302 -0
- solvapay/agents/identity.py +293 -0
- solvapay/agents/mcp.py +614 -0
- solvapay/agents/oauth.py +105 -0
- solvapay/business_details.py +124 -0
- solvapay/client.py +107 -0
- solvapay/errors.py +33 -0
- solvapay/mcp/__init__.py +90 -0
- solvapay/mcp/app.py +243 -0
- solvapay/mcp/auth.py +604 -0
- solvapay/mcp/bootstrap.py +494 -0
- solvapay/mcp/intent.py +474 -0
- solvapay/mcp/mcp-app.html +119 -0
- solvapay/mcp/narrate.py +1019 -0
- solvapay/mcp/response.py +220 -0
- solvapay/mcp/server.py +343 -0
- solvapay/mcp/tools.py +273 -0
- solvapay/mcp/transport.py +550 -0
- solvapay/money.py +61 -0
- solvapay/paywall/__init__.py +25 -0
- solvapay/paywall/decide.py +450 -0
- solvapay/paywall/gate.py +176 -0
- solvapay/paywall/state.py +371 -0
- solvapay/pricing.py +281 -0
- solvapay/py.typed +0 -0
- solvapay/solvapay.py +173 -0
- solvapay/webhooks.py +93 -0
- solvapay-0.1.0.dev4.dist-info/METADATA +183 -0
- solvapay-0.1.0.dev4.dist-info/RECORD +39 -0
- solvapay-0.1.0.dev4.dist-info/WHEEL +4 -0
solvapay/mcp/intent.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""The built-in ``account`` and ``activate_plan`` tools, the slash-command prompts and the
|
|
2
|
+
``docs://solvapay/overview.md`` resource, ported from ``@solvapay/mcp-core``'s
|
|
3
|
+
``descriptors.ts``.
|
|
4
|
+
|
|
5
|
+
``create_solvapay_mcp_server`` registers all of them. ``register_intent_tools``
|
|
6
|
+
adds them to any ``MCPServer``.
|
|
7
|
+
|
|
8
|
+
With ``resource_uri`` (``create_solvapay_mcp_server`` passes it), ``account`` opens
|
|
9
|
+
the SolvaPay widget on MCP Apps hosts: the tool lists ``_meta.ui.resourceUri`` and
|
|
10
|
+
its result carries it, except in ``mode: 'text'``. Text-only hosts read the same
|
|
11
|
+
narration either way. The widget's own tools are in ``transport.py``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import inspect
|
|
17
|
+
import json
|
|
18
|
+
import uuid
|
|
19
|
+
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import Annotated, Any, Literal
|
|
22
|
+
|
|
23
|
+
from mcp.server.mcpserver import Context, MCPServer
|
|
24
|
+
from mcp.server.mcpserver.prompts import Prompt
|
|
25
|
+
from mcp.types import CallToolResult, ResourceLink, TextContent, ToolAnnotations
|
|
26
|
+
from pydantic import BeforeValidator, Field, WithJsonSchema
|
|
27
|
+
|
|
28
|
+
from .._js import get
|
|
29
|
+
from ..errors import SolvaPayError
|
|
30
|
+
from ..solvapay import SolvaPay
|
|
31
|
+
from .app import tool_ui_meta
|
|
32
|
+
from .bootstrap import (
|
|
33
|
+
ADVERTISED_VIEWS,
|
|
34
|
+
ViewKind,
|
|
35
|
+
build_bootstrap_payload,
|
|
36
|
+
derive_default_view,
|
|
37
|
+
signed_in_customer_ref,
|
|
38
|
+
)
|
|
39
|
+
from .narrate import ACTIVATE_PLAN_TOOL_NAME, NARRATORS, VIEWER_TOOL_NAME, narrate_activate_plan
|
|
40
|
+
from .narrate import ui_placeholder as _ui_placeholder
|
|
41
|
+
from .response import tool_error_result
|
|
42
|
+
from .tools import default_customer_ref
|
|
43
|
+
|
|
44
|
+
Json = dict[str, Any]
|
|
45
|
+
Mode = Literal["ui", "text", "auto"]
|
|
46
|
+
GetCustomerRef = Callable[[Mapping[str, Any], Context], "str | Awaitable[str | None] | None"]
|
|
47
|
+
|
|
48
|
+
OVERVIEW_URI = "docs://solvapay/overview.md"
|
|
49
|
+
OVERVIEW_MIME_TYPE = "text/markdown"
|
|
50
|
+
OVERVIEW_MARKDOWN = """# SolvaPay MCP server — overview
|
|
51
|
+
|
|
52
|
+
This MCP server connects a SolvaPay-protected product to your host so users can
|
|
53
|
+
manage their plan, usage, and billing **without leaving the chat**. It is
|
|
54
|
+
dual-audience: every tool returns a UI bootstrap for hosts that render MCP UI
|
|
55
|
+
resources (`basic-host`, Claude Desktop, ChatGPT, etc.) and a markdown summary
|
|
56
|
+
with clickable URLs for text-only hosts.
|
|
57
|
+
|
|
58
|
+
## What the user can do
|
|
59
|
+
|
|
60
|
+
- **Account** — one read-only viewer for every billing surface. Pass `view`:
|
|
61
|
+
`checkout` (upgrade / change plan / buy / subscribe), `account` (current
|
|
62
|
+
plan, cancel, billing), or `topup` (add credits). Omit `view` and the
|
|
63
|
+
server picks (no plan → checkout, out of credits → topup, else account).
|
|
64
|
+
Slash commands `/upgrade`, `/manage_account`, and `/topup` remap onto
|
|
65
|
+
this tool. `account`.
|
|
66
|
+
- **Activate plan** — activate a specific plan by `planRef`. Free plans
|
|
67
|
+
activate immediately, usage-based plans activate when balance covers the
|
|
68
|
+
configured usage, paid plans return checkout. Requires `planRef` — to list
|
|
69
|
+
plans, call `account` with `view: "checkout"`. `/activate_plan`.
|
|
70
|
+
|
|
71
|
+
## How it fits together
|
|
72
|
+
|
|
73
|
+
The viewer returns a full `BootstrapPayload` (merchant + product + plans
|
|
74
|
+
+ customer snapshot) so the embedded UI never fires per-view read calls. When
|
|
75
|
+
a paywalled data tool hits the usage limit, its response is plain text: the
|
|
76
|
+
`content[0].text` narration names the recovery tool (`account`, or
|
|
77
|
+
`activate_plan` when a specific planRef is known) and inlines
|
|
78
|
+
`gate.checkoutUrl` for terminal-first hosts. The structured gate rides on
|
|
79
|
+
`structuredContent` for programmatic consumers. No widget iframe opens for a
|
|
80
|
+
gate — the LLM reads the narration and, if the user agrees, calls the named
|
|
81
|
+
tool which mounts the checkout / topup / account surface.
|
|
82
|
+
|
|
83
|
+
Auth is handled by the SolvaPay OAuth bridge (see `createMcpOAuthBridge`). The
|
|
84
|
+
bridge injects `customer_ref` onto every authenticated request; tools that
|
|
85
|
+
need an authenticated caller return `Unauthorized` when it is missing.
|
|
86
|
+
|
|
87
|
+
## Also see
|
|
88
|
+
|
|
89
|
+
- `docs://solvapay/overview.md` (this resource) — agent-facing narration.
|
|
90
|
+
- `ui://<app>/mcp-app.html` — the embedded UI shell.
|
|
91
|
+
- `tools/list` + `prompts/list` — programmatic discovery of the intent tools
|
|
92
|
+
and their slash-command shortcuts.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
VIEW_PARAM_DESCRIPTION = (
|
|
96
|
+
'Landing surface. checkout: user says "upgrade", "change plan", "buy", or "subscribe". '
|
|
97
|
+
'account: user says "my account", "current plan", "cancel", or "billing". '
|
|
98
|
+
'topup: user says "top up", "add credits", or "buy credits". '
|
|
99
|
+
"Omit to let the server pick (no plan → checkout, out of credits → topup, else account)."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
VIEWER_DESCRIPTION = (
|
|
103
|
+
'Call when the user says "upgrade", "change plan", "buy", "subscribe", "my account", '
|
|
104
|
+
'"current plan", "cancel", "billing", "top up", "add credits", or "buy credits". '
|
|
105
|
+
"Opens the SolvaPay billing surface. Pass `view` to pick the landing screen; omit it to "
|
|
106
|
+
"let the server pick (no plan → checkout, out of credits → topup, else account). "
|
|
107
|
+
"Read-only snapshot — charges happen after the customer confirms. Always include the "
|
|
108
|
+
"Manage and Checkout markdown links from the tool result in your reply to the user. "
|
|
109
|
+
"To change something, call this tool again with `view`: `account` (manage or cancel), "
|
|
110
|
+
"`topup` (add credits), or `checkout` (change plan)."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
MODE_PARAM_DESCRIPTION = (
|
|
114
|
+
"Default `mode: 'auto'` returns a self-sufficient text summary (plan, price, https "
|
|
115
|
+
"checkout URL) and still opens the iframe on UI hosts. Pass `mode: 'text'` to strip "
|
|
116
|
+
"the iframe, or `mode: 'ui'` for a one-line placeholder that still includes the "
|
|
117
|
+
"checkout URL."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
ACTIVATE_PLAN_DESCRIPTION = (
|
|
121
|
+
'Call when the user says "activate" and a specific `planRef` is known. Free plans '
|
|
122
|
+
"activate immediately; usage-based plans activate when the balance covers usage; paid "
|
|
123
|
+
"plans open checkout. Requires `planRef` — to list or pick a plan, call `account` with "
|
|
124
|
+
'view: "checkout".'
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
_CONTEXT_PARAM = "solvapay_mcp_context"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _json(data: Any) -> str:
|
|
131
|
+
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def parse_mode(raw: Any) -> Mode:
|
|
135
|
+
return raw if raw in ("ui", "text", "auto") else "auto"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def narrated_tool_result(
|
|
139
|
+
view: str,
|
|
140
|
+
data: Json,
|
|
141
|
+
mode: Mode = "auto",
|
|
142
|
+
meta: Json | None = None,
|
|
143
|
+
*,
|
|
144
|
+
now: datetime | None = None,
|
|
145
|
+
) -> CallToolResult:
|
|
146
|
+
"""``narratedToolResult``: the narration (led by a one-line placeholder in ``ui`` mode),
|
|
147
|
+
one ``resource_link`` per link, and the payload as structured content."""
|
|
148
|
+
narrator = NARRATORS.get(view)
|
|
149
|
+
if narrator is None:
|
|
150
|
+
return CallToolResult(
|
|
151
|
+
content=[TextContent(type="text", text=_json(data))],
|
|
152
|
+
structured_content=data,
|
|
153
|
+
meta=meta,
|
|
154
|
+
)
|
|
155
|
+
output = narrator(data, now)
|
|
156
|
+
narrated = TextContent(type="text", text=output.text)
|
|
157
|
+
links = [
|
|
158
|
+
ResourceLink(type="resource_link", uri=link["uri"], name=link["name"])
|
|
159
|
+
for link in output.links
|
|
160
|
+
]
|
|
161
|
+
content: list[Any] = [narrated, *links]
|
|
162
|
+
if mode == "ui":
|
|
163
|
+
content.insert(0, TextContent(type="text", text=_ui_placeholder(view, data)))
|
|
164
|
+
if mode == "text" and meta and "ui" in meta:
|
|
165
|
+
meta = {key: value for key, value in meta.items() if key != "ui"}
|
|
166
|
+
return CallToolResult(content=content, structured_content=data, meta=meta)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def _resolve_customer(
|
|
170
|
+
get_customer_ref: GetCustomerRef, args: Mapping[str, Any], context: Context
|
|
171
|
+
) -> str | None:
|
|
172
|
+
value = get_customer_ref(args, context)
|
|
173
|
+
resolved = await value if inspect.isawaitable(value) else value
|
|
174
|
+
return signed_in_customer_ref(resolved)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _error_from(error: SolvaPayError) -> CallToolResult:
|
|
178
|
+
status = error.status if error.status is not None else 500
|
|
179
|
+
return tool_error_result(error.message, status, error.message)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _annotate(tool: Callable[..., Any], params: list[inspect.Parameter]) -> None:
|
|
183
|
+
"""Gives ``tool`` the signature ``MCPServer`` derives its input schema from."""
|
|
184
|
+
params = [
|
|
185
|
+
*params,
|
|
186
|
+
inspect.Parameter(_CONTEXT_PARAM, inspect.Parameter.KEYWORD_ONLY, annotation=Context),
|
|
187
|
+
]
|
|
188
|
+
tool.__signature__ = inspect.Signature(params, return_annotation=CallToolResult) # type: ignore[attr-defined]
|
|
189
|
+
tool.__annotations__ = {p.name: p.annotation for p in params} | {"return": CallToolResult}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def register_intent_tools(
|
|
193
|
+
server: MCPServer,
|
|
194
|
+
*,
|
|
195
|
+
solvapay: SolvaPay,
|
|
196
|
+
product_ref: str,
|
|
197
|
+
public_base_url: str,
|
|
198
|
+
views: Iterable[ViewKind] = ADVERTISED_VIEWS,
|
|
199
|
+
get_customer_ref: GetCustomerRef | None = None,
|
|
200
|
+
resource_uri: str | None = None,
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Registers ``account`` (unless ``views`` is empty), ``activate_plan``, the
|
|
203
|
+
``/upgrade``, ``/manage_account``, ``/topup`` and ``/activate_plan`` prompts, and the
|
|
204
|
+
``docs://solvapay/overview.md`` resource.
|
|
205
|
+
|
|
206
|
+
``resource_uri`` is the widget's ``ui://`` resource, which the caller registers
|
|
207
|
+
(``create_solvapay_mcp_server`` does). Without it ``account`` is text only."""
|
|
208
|
+
if not public_base_url.startswith(("https://", "http://")):
|
|
209
|
+
raise SolvaPayError("public_base_url must be an http(s) URL", code="invalid_argument")
|
|
210
|
+
enabled = set(views)
|
|
211
|
+
unknown = enabled - set(ADVERTISED_VIEWS)
|
|
212
|
+
if unknown:
|
|
213
|
+
raise SolvaPayError(
|
|
214
|
+
f"Unknown views: {', '.join(sorted(unknown))}. Use checkout, account or topup.",
|
|
215
|
+
code="invalid_argument",
|
|
216
|
+
)
|
|
217
|
+
resolve = get_customer_ref or default_customer_ref
|
|
218
|
+
advertised = [view for view in ADVERTISED_VIEWS if view in enabled]
|
|
219
|
+
|
|
220
|
+
async def payload(view: ViewKind, customer_ref: str | None) -> Json:
|
|
221
|
+
return await build_bootstrap_payload(
|
|
222
|
+
solvapay,
|
|
223
|
+
view=view,
|
|
224
|
+
product_ref=product_ref,
|
|
225
|
+
public_base_url=public_base_url,
|
|
226
|
+
customer_ref=customer_ref,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if advertised:
|
|
230
|
+
|
|
231
|
+
async def account(**kwargs: Any) -> CallToolResult:
|
|
232
|
+
context: Context = kwargs.pop(_CONTEXT_PARAM)
|
|
233
|
+
requested = kwargs.get("view")
|
|
234
|
+
if requested is not None and requested != "auto-recharge" and requested not in enabled:
|
|
235
|
+
return tool_error_result(
|
|
236
|
+
f"view '{requested}' is not enabled on this server",
|
|
237
|
+
400,
|
|
238
|
+
f"Enabled views: {', '.join(advertised)}. Pass one of those, or omit "
|
|
239
|
+
"view to let the server pick.",
|
|
240
|
+
)
|
|
241
|
+
mode = parse_mode(kwargs.get("mode"))
|
|
242
|
+
customer_ref = await _resolve_customer(resolve, kwargs, context)
|
|
243
|
+
try:
|
|
244
|
+
if requested is None:
|
|
245
|
+
# Built as 'account': the view only steers the checkout purpose.
|
|
246
|
+
data = await payload("account", customer_ref)
|
|
247
|
+
data["view"] = derive_default_view(data, enabled)
|
|
248
|
+
else:
|
|
249
|
+
data = await payload(requested, customer_ref)
|
|
250
|
+
except SolvaPayError as error:
|
|
251
|
+
return _error_from(error)
|
|
252
|
+
result_meta: Json = {"openai/widgetSessionId": str(uuid.uuid4())}
|
|
253
|
+
if resource_uri is not None:
|
|
254
|
+
result_meta = {"ui": {"resourceUri": resource_uri}, **result_meta}
|
|
255
|
+
return narrated_tool_result(data["view"], data, mode, result_meta)
|
|
256
|
+
|
|
257
|
+
# An enum even for one view: pydantic would publish a single Literal as `const`.
|
|
258
|
+
view_type: Any = Annotated[
|
|
259
|
+
Literal[tuple(advertised)], WithJsonSchema({"type": "string", "enum": advertised})
|
|
260
|
+
]
|
|
261
|
+
_annotate(
|
|
262
|
+
account,
|
|
263
|
+
[
|
|
264
|
+
inspect.Parameter(
|
|
265
|
+
"view",
|
|
266
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
267
|
+
default=None,
|
|
268
|
+
annotation=Annotated[
|
|
269
|
+
view_type | None,
|
|
270
|
+
# An old transcript's 'auto-recharge' still parses, as 'account'.
|
|
271
|
+
BeforeValidator(lambda v: "account" if v == "auto-recharge" else v),
|
|
272
|
+
Field(description=VIEW_PARAM_DESCRIPTION),
|
|
273
|
+
],
|
|
274
|
+
),
|
|
275
|
+
inspect.Parameter(
|
|
276
|
+
"mode",
|
|
277
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
278
|
+
default=None,
|
|
279
|
+
annotation=Annotated[Mode | None, Field(description=MODE_PARAM_DESCRIPTION)],
|
|
280
|
+
),
|
|
281
|
+
],
|
|
282
|
+
)
|
|
283
|
+
server.add_tool(
|
|
284
|
+
account,
|
|
285
|
+
name=VIEWER_TOOL_NAME,
|
|
286
|
+
title="Account",
|
|
287
|
+
description=VIEWER_DESCRIPTION,
|
|
288
|
+
annotations=ToolAnnotations(
|
|
289
|
+
read_only_hint=True, idempotent_hint=True, open_world_hint=True
|
|
290
|
+
),
|
|
291
|
+
meta=tool_ui_meta(resource_uri) if resource_uri is not None else None,
|
|
292
|
+
structured_output=False,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
async def activate_plan(**kwargs: Any) -> CallToolResult:
|
|
296
|
+
context: Context = kwargs.pop(_CONTEXT_PARAM)
|
|
297
|
+
product_arg = kwargs.get("productRef")
|
|
298
|
+
product = product_arg if isinstance(product_arg, str) and product_arg else product_ref
|
|
299
|
+
plan_arg = kwargs.get("planRef")
|
|
300
|
+
plan_ref = plan_arg.strip() if isinstance(plan_arg, str) else ""
|
|
301
|
+
if not plan_ref:
|
|
302
|
+
return tool_error_result(
|
|
303
|
+
"activate_plan requires a planRef",
|
|
304
|
+
400,
|
|
305
|
+
"Pass `planRef` to activate a specific plan. To list plans, call `account` "
|
|
306
|
+
'with view: "checkout".',
|
|
307
|
+
)
|
|
308
|
+
customer_ref = await _resolve_customer(resolve, kwargs, context)
|
|
309
|
+
if customer_ref is None:
|
|
310
|
+
return tool_error_result(
|
|
311
|
+
"Unauthorized", 401, "customer_ref missing from MCP auth context"
|
|
312
|
+
)
|
|
313
|
+
api = solvapay.api
|
|
314
|
+
try:
|
|
315
|
+
response = await api.activate_plan(
|
|
316
|
+
body={"customerRef": customer_ref, "productRef": product, "planRef": plan_ref}
|
|
317
|
+
)
|
|
318
|
+
except SolvaPayError as error:
|
|
319
|
+
return _error_from(error)
|
|
320
|
+
result: Json = response.model_dump(mode="json", by_alias=True, exclude_unset=True)
|
|
321
|
+
solvapay.paywall.invalidate_limits(customer_ref, product)
|
|
322
|
+
|
|
323
|
+
checkout_url = get(result, "checkoutUrl")
|
|
324
|
+
if result.get("status") == "payment_required" and not checkout_url:
|
|
325
|
+
try:
|
|
326
|
+
session = await api.create_checkout_session(
|
|
327
|
+
body={"productRef": product, "customerRef": customer_ref, "planRef": plan_ref}
|
|
328
|
+
)
|
|
329
|
+
checkout_url = session.checkout_url
|
|
330
|
+
except SolvaPayError:
|
|
331
|
+
pass # TypeScript answers without a link too; the narration says to call account.
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
listed = (await api.list_plans(product_ref=product)).plans or []
|
|
335
|
+
plan_name = next((p.name for p in listed if p.reference == plan_ref), None)
|
|
336
|
+
except SolvaPayError:
|
|
337
|
+
plan_name = None # The name only labels the narration.
|
|
338
|
+
|
|
339
|
+
narrated: Json = {**result}
|
|
340
|
+
if isinstance(checkout_url, str):
|
|
341
|
+
narrated["checkoutUrl"] = checkout_url
|
|
342
|
+
if plan_name is not None:
|
|
343
|
+
narrated["planName"] = plan_name
|
|
344
|
+
structured = {**result}
|
|
345
|
+
if isinstance(checkout_url, str) and checkout_url:
|
|
346
|
+
structured["checkoutUrl"] = checkout_url
|
|
347
|
+
return CallToolResult(
|
|
348
|
+
content=[TextContent(type="text", text=narrate_activate_plan(narrated))],
|
|
349
|
+
structured_content=structured,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
_annotate(
|
|
353
|
+
activate_plan,
|
|
354
|
+
[
|
|
355
|
+
inspect.Parameter(
|
|
356
|
+
"planRef",
|
|
357
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
358
|
+
annotation=Annotated[str, Field(description="Plan to activate. Required.")],
|
|
359
|
+
),
|
|
360
|
+
inspect.Parameter(
|
|
361
|
+
"productRef", inspect.Parameter.KEYWORD_ONLY, default=None, annotation=str | None
|
|
362
|
+
),
|
|
363
|
+
],
|
|
364
|
+
)
|
|
365
|
+
server.add_tool(
|
|
366
|
+
activate_plan,
|
|
367
|
+
name=ACTIVATE_PLAN_TOOL_NAME,
|
|
368
|
+
title="Activate plan",
|
|
369
|
+
description=ACTIVATE_PLAN_DESCRIPTION,
|
|
370
|
+
annotations=ToolAnnotations(
|
|
371
|
+
read_only_hint=False,
|
|
372
|
+
destructive_hint=False,
|
|
373
|
+
idempotent_hint=True,
|
|
374
|
+
open_world_hint=True,
|
|
375
|
+
),
|
|
376
|
+
meta={"openai/widgetAccessible": True},
|
|
377
|
+
structured_output=False,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
for prompt in build_solvapay_prompts(enabled):
|
|
381
|
+
server.add_prompt(prompt)
|
|
382
|
+
|
|
383
|
+
server.resource(
|
|
384
|
+
OVERVIEW_URI,
|
|
385
|
+
name="SolvaPay MCP — overview",
|
|
386
|
+
title="SolvaPay overview",
|
|
387
|
+
description=(
|
|
388
|
+
'Agent-facing "start here" doc — explains the two intent tools, dual-audience '
|
|
389
|
+
"fallback, and auth model before any tool is called."
|
|
390
|
+
),
|
|
391
|
+
mime_type=OVERVIEW_MIME_TYPE,
|
|
392
|
+
)(lambda: OVERVIEW_MARKDOWN)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def build_solvapay_prompts(enabled_views: Iterable[ViewKind] = ADVERTISED_VIEWS) -> list[Prompt]:
|
|
396
|
+
"""The slash-command prompts: one user message each, naming the tool to call."""
|
|
397
|
+
enabled = set(enabled_views)
|
|
398
|
+
viewer = f"Call the `{VIEWER_TOOL_NAME}` tool with view:"
|
|
399
|
+
prompts: list[Prompt] = []
|
|
400
|
+
|
|
401
|
+
if "checkout" in enabled:
|
|
402
|
+
|
|
403
|
+
def upgrade(planRef: str | None = None) -> str:
|
|
404
|
+
if planRef:
|
|
405
|
+
return f'{viewer} "checkout", then activate plan {planRef}.'
|
|
406
|
+
return f'{viewer} "checkout" to show upgrade options.'
|
|
407
|
+
|
|
408
|
+
prompts.append(
|
|
409
|
+
Prompt.from_function(
|
|
410
|
+
upgrade,
|
|
411
|
+
name="upgrade",
|
|
412
|
+
title="Upgrade plan",
|
|
413
|
+
description="Start or change a paid plan for the current customer.",
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if "account" in enabled:
|
|
418
|
+
|
|
419
|
+
def manage_account() -> str:
|
|
420
|
+
return f'{viewer} "account" to show my SolvaPay account.'
|
|
421
|
+
|
|
422
|
+
prompts.append(
|
|
423
|
+
Prompt.from_function(
|
|
424
|
+
manage_account,
|
|
425
|
+
name="manage_account",
|
|
426
|
+
title="Manage account",
|
|
427
|
+
description=(
|
|
428
|
+
"Show the current plan, balance, payment method, and cancel/reactivate "
|
|
429
|
+
"controls for the current customer."
|
|
430
|
+
),
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if "topup" in enabled:
|
|
435
|
+
|
|
436
|
+
def topup(amount: str | None = None) -> str:
|
|
437
|
+
if amount:
|
|
438
|
+
return f'{viewer} "topup" and top up my credits by {amount}.'
|
|
439
|
+
return f'{viewer} "topup" to add SolvaPay credits.'
|
|
440
|
+
|
|
441
|
+
prompts.append(
|
|
442
|
+
Prompt.from_function(
|
|
443
|
+
topup,
|
|
444
|
+
name="topup",
|
|
445
|
+
title="Top up credits",
|
|
446
|
+
description="Add SolvaPay credits to the current customer.",
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def activate(planRef: str | None = None) -> str:
|
|
451
|
+
if planRef:
|
|
452
|
+
return f"Call the `{ACTIVATE_PLAN_TOOL_NAME}` tool with planRef {planRef}."
|
|
453
|
+
return f'{viewer} "checkout" to list plans I can activate.'
|
|
454
|
+
|
|
455
|
+
prompts.append(
|
|
456
|
+
Prompt.from_function(
|
|
457
|
+
activate,
|
|
458
|
+
name="activate_plan",
|
|
459
|
+
title="Activate plan",
|
|
460
|
+
description="Activate a specific plan by ref, or list plans to pick from.",
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
return prompts
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
__all__ = [
|
|
467
|
+
"OVERVIEW_MARKDOWN",
|
|
468
|
+
"OVERVIEW_URI",
|
|
469
|
+
"build_solvapay_prompts",
|
|
470
|
+
"narrated_tool_result",
|
|
471
|
+
"parse_mode",
|
|
472
|
+
"register_intent_tools",
|
|
473
|
+
"tool_error_result",
|
|
474
|
+
]
|