@telaro/sacp-mcp-server
v0.1.0
Published
Real MCP server (JSON-RPC over stdio / Streamable HTTP) backed by sACP. Each tool call charges a prepaid credit balance and auto-funds + settles a sACP Job behind the scenes, so MCP hosts (Claude desktop, Claude.ai, ChatGPT) can call sACP-paid agents the
Readme
@telaro/sacp-mcp-server
Real MCP server (@modelcontextprotocol/sdk based, JSON-RPC) backed
by sACP. Drop it into Claude desktop's config or Claude.ai's remote
MCP and every tools/call charges a prepaid credit balance, funds a
sACP Job, runs the agent work, and settles. The MCP host never sees
the payment step.
What this fixes
The earlier @telaro/sacp-mcp package was a marketplace-style
endpoint with a custom /mcp/invoke route. Hosts that follow the
real MCP spec (Claude desktop, Claude.ai, ChatGPT MCP) couldn't
register or call it.
This package speaks JSON-RPC initialize / tools/list /
tools/call over the SDK's standard transports:
- stdio: Claude desktop spawns the binary and talks to it on stdin/stdout.
- Streamable HTTP: Claude.ai's remote MCP + ChatGPT integrations
POST to a single
/mcpendpoint.
Architecture
┌─────────────────┐ JSON-RPC ┌──────────────────────────┐
│ Claude desktop │ ─────────────► │ @telaro/sacp-mcp-server│
└─────────────────┘ │ │
│ tools/call: │
│ 1. credit.charge │
│ 2. createJobFrom + │
│ session.fund │
│ 3. workHandler(...) │
│ 4. session.submitWork │
│ 5. session.accept │
└──────────────┬───────────┘
│
┌────────▼────────┐
│ Solana sACP │
│ Job + escrow │
└─────────────────┘Operator-side bookkeeping:
Stripe webhook / on-chain deposit / manual ops
│
▼
creditStore.topUp(userId, atoms)
│
▼
tools/call → credit.charge → on-chain JobWire it into Claude desktop
{
"mcpServers": {
"sacp": {
"command": "node",
"args": ["/abs/path/packages/sacp-mcp-server/dist/cli.js"],
"env": {
"SACP_RPC": "https://api.mainnet-beta.solana.com",
"SACP_BUYER_KEYPAIR": "/abs/path/buyer.json",
"SACP_PROVIDER_KEYPAIR": "/abs/path/provider.json",
"SACP_EVALUATOR_KEYPAIR": "/abs/path/evaluator.json",
"SACP_OFFERING_PROVIDER": "<base58 provider pubkey>",
"SACP_OFFERING_SLOT": "1",
"SACP_API_KEY": "<long random secret>",
"SACP_INITIAL_CREDIT_ATOMS": "10000000",
"SACP_USER_API_KEY": "<same long random secret>"
}
}
}
}SACP_USER_API_KEY is what the server-side library expects the host
to forward. Claude desktop currently doesn't surface per-tool auth,
so the simplest deploy is one server process per Claude installation
with a unique API key + dedicated credit account. Multi-tenant
deploys swap in a real ApiKeyResolver that maps OAuth/Stripe
identity to the userId.
For Claude.ai or ChatGPT remote MCP:
SACP_MCP_TRANSPORT=http PORT=8788 \
SACP_RPC=https://api.mainnet-beta.solana.com \
SACP_BUYER_KEYPAIR=/etc/sacp/buyer.json \
SACP_PROVIDER_KEYPAIR=/etc/sacp/provider.json \
SACP_EVALUATOR_KEYPAIR=/etc/sacp/evaluator.json \
SACP_OFFERING_PROVIDER=... \
SACP_OFFERING_SLOT=1 \
SACP_API_KEY=... \
SACP_INITIAL_CREDIT_ATOMS=10000000 \
node dist/cli.jsPoint the remote MCP integration at https://your-host:8788/mcp.
Programmatic use
import { Connection, Keypair } from "@solana/web3.js";
import { SacpClient, LocalSigner } from "@telaro/sacp";
import {
SacpMcpServer,
InMemoryCreditStore,
StaticApiKeyResolver,
startStreamableHttp,
} from "@telaro/sacp-mcp-server";
const sacp = new SacpClient({ connection: new Connection(rpc) });
const offering = await sacp.offering.fetch(provider.publicKey, slotId);
const server = new SacpMcpServer({
client: sacp,
offering: offering!,
buyerSigner: new LocalSigner(buyer),
providerSigner: new LocalSigner(provider),
evaluatorSigner: new LocalSigner(evaluator),
credit: new InMemoryCreditStore(),
auth: StaticApiKeyResolver.singleUser(process.env.SACP_API_KEY!, "alice"),
tools: [
{
name: "summarize_image",
description: "Return a 3-line summary of the image at the given URL.",
inputSchema: {
type: "object",
properties: { image_url: { type: "string" } },
required: ["image_url"],
},
handler: async ({ image_url }) => ({
result: await summarize(String(image_url)),
submissionUri: await uploadResultToIpfs(...),
}),
},
],
});
await startStreamableHttp(server, { port: 8788 });Credit + auth
InMemoryCreditStore ships with the package for first deployments
and the included end-to-end smoke. For production, write a Postgres
or Redis-backed implementation of the CreditStore interface and
swap it in.
StaticApiKeyResolver maps a fixed set of API keys to userIds. For
real multi-tenant operation, implement ApiKeyResolver against your
auth source (OAuth, JWT, Stripe customer id, etc.).
Both interfaces are intentionally minimal so the server can be lifted into any operator stack.
Insufficient credit behavior
When credit can't cover the tool's price, the server returns a
standard MCP isError: true result with content[0].text describing
the shortfall. Claude desktop / Claude.ai surface this to the user
as a normal tool failure. The host is not asked to pay anything; the
operator's top-up flow (Stripe, manual deposit, etc.) is the user's
remediation path.
What this is not
- Not a Stripe integration. It's the credit primitive. Wire your
Stripe webhook handler to
creditStore.topUp(userId, atoms). - Not a multi-offering router. One server instance backs one Offering. Run multiple instances behind different MCP server names to expose multiple Offerings.
- Not a dispute resolver. Disputes go through the standard sACP
EvaluatorRuntime+submit_verdictpath. The server's job is to settle happy paths; unhappy paths fall through to the protocol.
