Provider-agnostic payment layer for MCP (Model Context Protocol) tools and agents
PayMCP defines a single dynamically-registered tool 'confirm_<tool_name>_<payment_id>' that wraps payment confirmation workflows. However, the tool definition is INFERRED from code logic rather than explicitly registered with a visible schema. The tool lacks a proper input schema (no parameters declared), has only a minimal description, and the registration happens at runtime via decorator patching of MCP SDK internals. The codebase explicitly patches SDK internals due to missing APIs (line: 'MCP SDK Compatibility: This implementation patches MCP SDK internals because: 1. SDK has no post-init capability registration API (v1.x)'), which signals non-standard tool registration and makes the tool definition unreliable for static analysis. Parameter descriptions are absent, error responses exist but lack structured guidance, and the tool is fundamentally a wrapper that hides/shows tools dynamically rather than a first-class agent tool with clear semantics.
Confirm payment and execute the original tool after payment is completed
Tool definition is inferred from runtime code, not explicitly registered with schema. The @mcp.tool() decorator inside _initiate_wrapper() registers the confirmation tool dynamically, making static schema analysis impossible. No JSON Schema input definition visible.
No input schema defined for confirm_<tool_name>_<payment_id>. The tool accepts a single optional 'ctx' parameter but declares no schema, types, or validation.
Tool name contains dynamic placeholders (<tool_name>, <payment_id>) making it impossible for LLMs to understand intent from the name alone. Names like 'confirm_send_email_pay_xyz123' do not follow verb_noun convention and obscure the actual action.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-22 | F | 29 | <=2025-11-25 | v2 |
| 2026-03-09 | F | 15 | - | v1 |
Description is minimal: 'Confirm payment and execute the original tool after payment is completed'.
No documented output schema. The _confirm() function returns a dict with fields like 'status', 'message', 'payment_id', 'content' (with type 'text'), and conditionally 'annotations' and 'payment_url'. LLMs cannot plan downstream calls or extract data without knowing these fields exist.
Error responses lack recovery guidance. When payment status is not 'paid', the tool returns 'Inform user: Payment not yet completed. Current status: {status}. Ask them to complete payment at: {payment_url}', this is narrative text, not structured error classification. The classification drives the agent's next step.'
MCP SDK patching: Code modifies internal SDK structures (_tool_manager._tools) and relies on undocumented request_ctx methods. This is not portable and will break on SDK updates. Per code comment: 'Monitor: https://github.com/modelcontextprotocol/python-sdk for future APIs. If SDK adds hooks/filters, we can remove patches and use official APIs.'
Stateful tool registration. The PAYMENTS, HIDDEN_TOOLS, and CONFIRMATION_TOOLS dictionaries maintain in-memory session state (payment_id -> PaymentSession, session_id -> hidden_tools_set, confirm_tool_name -> session_id). This violates stateless request handling; if the server restarts or scales horizontally, all payment state is lost. Per 2026-07-28 spec: state should not be maintained across requests.
Payment state cleanup is not transaction-safe. If _confirm() fails after del PAYMENTS[pid] but before cleanup completes, the tool may be left in hidden state or orphaned. No idempotency guarantee or rollback mechanism.
Payment provider is selected with 'next((v for k, v in providers.items() if k != "x402"), None)', fragile logic that picks the first non-x402 provider. No validation that the provider exists or is configured. If providers dict is empty, raises RuntimeError with no recovery path.
Parameter 'ctx' is optional and silently defaults to None if missing. When ctx is None, get_stable_session_id(ctx) is called which may fail or return None, raising RuntimeError 'No Session ID provided.' This is not a friendly error, LLMs cannot self-correct.