budgent-mcp 0.1.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.
Files changed (3) hide show
  1. package/README.md +42 -0
  2. package/package.json +18 -0
  3. package/server.mjs +122 -0
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # budgent-mcp
2
+
3
+ A stdio [MCP](https://modelcontextprotocol.io) server that gives any MCP host (Claude Desktop, etc.)
4
+ the Budgent **pay** capability. Non-custodial: the agent holds only a scoped API key; the on-chain
5
+ program enforces the budget.
6
+
7
+ ## Tools
8
+
9
+ - `budgent_pay` — params: `amount` (required), `domain?`, `recipient?`, `resource?`, `taskId?`. Returns the on-chain verdict (`status` SETTLED/REVERTED/HELD + reason + signature/explorer).
10
+ - `budgent_balance` — no params. Returns the agent's budget snapshot (`GET /v1/me`).
11
+
12
+ ## Install & run
13
+
14
+ ```bash
15
+ npm i
16
+ BUDGENT_BASE_URL=https://api.budgent.xyz \
17
+ BUDGENT_KEY_ID=your_key_id \
18
+ BUDGENT_HMAC_SECRET=your_hmac_secret \
19
+ npm start
20
+ ```
21
+
22
+ ## Claude Desktop config
23
+
24
+ Add to `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "budgent": {
30
+ "command": "node",
31
+ "args": ["/absolute/path/to/budgent/sdk/mcp/server.mjs"],
32
+ "env": {
33
+ "BUDGENT_BASE_URL": "https://api.budgent.xyz",
34
+ "BUDGENT_KEY_ID": "your_key_id",
35
+ "BUDGENT_HMAC_SECRET": "your_hmac_secret"
36
+ }
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ Restart Claude Desktop; the `budgent_pay` and `budgent_balance` tools will be available to the agent.
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "budgent-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Budgent MCP server — gives any MCP host (Claude Desktop, etc.) a non-custodial, on-chain-enforced pay capability.",
5
+ "type": "module",
6
+ "bin": {
7
+ "budgent-mcp": "server.mjs"
8
+ },
9
+ "scripts": {
10
+ "start": "node server.mjs"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.0.0"
14
+ },
15
+ "engines": {
16
+ "node": ">=18"
17
+ }
18
+ }
package/server.mjs ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Budgent MCP server — exposes the Budgent pay capability to any MCP host
4
+ * (Claude Desktop, etc.) over stdio. Non-custodial: the agent holds only a
5
+ * scoped API key; the on-chain program enforces the budget.
6
+ *
7
+ * Config via env:
8
+ * BUDGENT_BASE_URL e.g. https://api.budgent.xyz
9
+ * BUDGENT_KEY_ID the scoped API key id
10
+ * BUDGENT_HMAC_SECRET the HMAC secret for that key
11
+ */
12
+ import { createHmac } from 'node:crypto';
13
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import {
16
+ CallToolRequestSchema,
17
+ ListToolsRequestSchema,
18
+ } from '@modelcontextprotocol/sdk/types.js';
19
+
20
+ const BASE_URL = process.env.BUDGENT_BASE_URL;
21
+ const KEY_ID = process.env.BUDGENT_KEY_ID;
22
+ const HMAC_SECRET = process.env.BUDGENT_HMAC_SECRET;
23
+
24
+ if (!BASE_URL || !KEY_ID || !HMAC_SECRET) {
25
+ console.error('Missing env: BUDGENT_BASE_URL, BUDGENT_KEY_ID, BUDGENT_HMAC_SECRET are required.');
26
+ process.exit(1);
27
+ }
28
+
29
+ /** Sign + send a request, matching the Budgent HMAC scheme exactly. */
30
+ async function budgentRequest(method, path, payload) {
31
+ const body = payload != null ? JSON.stringify(payload) : '';
32
+ const ts = Math.floor(Date.now() / 1000).toString();
33
+ const base = `${ts}.${method}.${path}.${body}`;
34
+ const sig = createHmac('sha256', HMAC_SECRET).update(base).digest('hex');
35
+ const res = await fetch(BASE_URL + path, {
36
+ method,
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'X-Budgent-Key': KEY_ID,
40
+ 'X-Budgent-Timestamp': ts,
41
+ 'X-Budgent-Signature': sig,
42
+ },
43
+ body: method === 'GET' ? undefined : body,
44
+ });
45
+ const text = await res.text();
46
+ const json = text ? JSON.parse(text) : {};
47
+ if (!res.ok) throw new Error(`${res.status} ${json.message || text}`);
48
+ return json;
49
+ }
50
+
51
+ function verdictText(r) {
52
+ const parts = [`status=${r.status}`, `reason=${r.reason}`, `amount=${r.amount} ${r.asset}`];
53
+ if (r.signature) parts.push(`signature=${r.signature}`);
54
+ if (r.explorer) parts.push(`explorer=${r.explorer}`);
55
+ return parts.join(' | ');
56
+ }
57
+
58
+ const TOOLS = [
59
+ {
60
+ name: 'budgent_pay',
61
+ description:
62
+ 'Pay for a resource through Budgent (non-custodial, on-chain enforced). ' +
63
+ 'Provide an amount and either a domain or a recipient. Returns the on-chain verdict.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ amount: { type: 'number', description: 'Amount to pay (vault asset units, e.g. USDC).' },
68
+ domain: { type: 'string', description: 'Resource domain resolved via the vault registry, e.g. "api.openai.com".' },
69
+ recipient: { type: 'string', description: 'Direct recipient pubkey, if not using a domain.' },
70
+ resource: { type: 'string', description: 'What is being paid for, e.g. "gpt-4o tokens".' },
71
+ taskId: { type: 'string', description: 'Caller task id for attribution / idempotency.' },
72
+ },
73
+ required: ['amount'],
74
+ },
75
+ },
76
+ {
77
+ name: 'budgent_balance',
78
+ description: "Get the agent's Budgent budget snapshot (policy, balance, spend so far).",
79
+ inputSchema: { type: 'object', properties: {} },
80
+ },
81
+ ];
82
+
83
+ const server = new Server(
84
+ { name: 'budgent-mcp', version: '0.1.0' },
85
+ { capabilities: { tools: {} } },
86
+ );
87
+
88
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
89
+
90
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
91
+ const { name, arguments: args = {} } = request.params;
92
+ try {
93
+ if (name === 'budgent_pay') {
94
+ const { amount, domain, recipient, resource, taskId } = args;
95
+ const payload = { amount };
96
+ if (domain != null) payload.domain = domain;
97
+ if (recipient != null) payload.recipient = recipient;
98
+ if (resource != null) payload.resource = resource;
99
+ if (taskId != null) payload.taskId = taskId;
100
+ const r = await budgentRequest('POST', '/v1/payments', payload);
101
+ return { content: [{ type: 'text', text: verdictText(r) }] };
102
+ }
103
+ if (name === 'budgent_balance') {
104
+ const r = await budgentRequest('GET', '/v1/me');
105
+ return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
106
+ }
107
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
108
+ } catch (e) {
109
+ return { content: [{ type: 'text', text: `${name} error: ${e?.message ?? String(e)}` }], isError: true };
110
+ }
111
+ });
112
+
113
+ async function main() {
114
+ const transport = new StdioServerTransport();
115
+ await server.connect(transport);
116
+ console.error('budgent-mcp server running on stdio');
117
+ }
118
+
119
+ main().catch((e) => {
120
+ console.error('budgent-mcp fatal:', e);
121
+ process.exit(1);
122
+ });