@paytaca/opencode-plugin 0.1.16 → 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 +42 -0
- package/dist/bundled/mcp.d.ts +2 -0
- package/dist/bundled/mcp.d.ts.map +1 -0
- package/dist/bundled/mcp.js +467 -0
- package/dist/bundled/mcp.js.map +1 -0
- package/dist/bundled/proxy.d.ts +1 -1
- package/dist/bundled/proxy.d.ts.map +1 -1
- package/dist/bundled/proxy.js +17 -10
- package/dist/bundled/proxy.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +4 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +99 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +1 -0
- package/dist/proxy.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,48 @@ Once installed and configured in your OpenCode settings, the plugin automaticall
|
|
|
34
34
|
- Creates a wallet on first run (recovery phrase is printed — save it securely)
|
|
35
35
|
- Starts a local proxy that manages x402 payment flows transparently
|
|
36
36
|
- Provides the `paytaca-ai` provider with the `deepseek/deepseek-v4-flash` model
|
|
37
|
+
- Registers a local MCP server (`paytaca`) whose tools let the assistant work with real Paytaca data — AI account (credits, models, plan pricing) and wallet (balance, transactions, addresses, tokens, sending)
|
|
38
|
+
|
|
39
|
+
### Asking about your account and wallet
|
|
40
|
+
|
|
41
|
+
Because the MCP server is loaded automatically, you can ask in plain language:
|
|
42
|
+
|
|
43
|
+
- "How many credits do I have left?"
|
|
44
|
+
- "What models are available?"
|
|
45
|
+
- "How much does DeepSeek V4 Pro cost?"
|
|
46
|
+
- "What's my wallet balance?"
|
|
47
|
+
- "Show my latest transactions"
|
|
48
|
+
- "What's my receiving address?"
|
|
49
|
+
|
|
50
|
+
The assistant answers using live data via these tools:
|
|
51
|
+
|
|
52
|
+
| Tool | Description |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `get_credits` | Remaining time credits per active model session |
|
|
55
|
+
| `get_balance` | BCH balance of the Paytaca wallet |
|
|
56
|
+
| `get_models` | Available models (id, display name, tier) |
|
|
57
|
+
| `get_plans` | Plan pricing grouped by tier (minutes, USD, BCH) |
|
|
58
|
+
| `get_transactions` | Recent wallet transactions (filter by direction, page) |
|
|
59
|
+
| `get_receiving_address` | Receiving address, optionally as a BIP21 URI with amount |
|
|
60
|
+
| `get_tokens` | CashToken holdings, or details for one token category |
|
|
61
|
+
|
|
62
|
+
### Sending funds
|
|
63
|
+
|
|
64
|
+
You can ask the assistant to send BCH or CashTokens ("send 0.01 BCH to
|
|
65
|
+
bitcoincash:qp..."). Because that spends real funds, opencode will always
|
|
66
|
+
prompt you for approval before the `send` tool executes — the plugin sets the
|
|
67
|
+
`paytaca_send` permission to `ask` (an explicit choice in your own config is
|
|
68
|
+
respected). Token amounts are in base units and recipients should use
|
|
69
|
+
token-aware (z-prefix) addresses.
|
|
70
|
+
|
|
71
|
+
### Proxy chatter never reaches the model
|
|
72
|
+
|
|
73
|
+
Payment prompts, tier-selection menus, credits output, and payment notices
|
|
74
|
+
produced by the proxy stay visible in your session for you — but they are
|
|
75
|
+
stripped from the context sent to the LLM, so coding conversations are not
|
|
76
|
+
polluted by payment flow messages. Your replies in those flows (e.g. picking
|
|
77
|
+
a plan tier) are also excluded, while all genuine coding messages pass
|
|
78
|
+
through untouched.
|
|
37
79
|
|
|
38
80
|
### Wallet management
|
|
39
81
|
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const MCP_SERVER_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca MCP Server\n *\n * Registers Paytaca tools with opencode so the assistant can work with real\n * data instead of guessing. Two groups:\n * - Paytaca AI account (backend): credits, models, plan pricing\n * - Paytaca wallet (paytaca CLI): balance, transactions, receiving address,\n * token holdings, and sending funds\n *\n * The send tool moves real funds \u2014 opencode is configured (via the plugin's\n * config hook) to require explicit user approval before it runs.\n *\n * Loaded automatically via the plugin's config hook (cfg.mcp['paytaca']).\n * Uses only Node.js built-in modules.\n */\n\nconst http = require('http');\nconst https = require('https');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\nconst CONFIG_DIR = process.env.PAYTACA_CONFIG_DIR || path.join(os.homedir(), '.opencode-paytaca');\nconst PAYTACA_CMD = process.env.PAYTACA_CMD || 'paytaca';\nconst DEFAULT_BACKEND = process.env.PAYTACA_BACKEND_URL || 'https://api.paytaca.ai';\n\nconst PROTOCOL_VERSION = '2025-06-18';\n\n// Logging setup\nconst LOG_FILE = path.join(CONFIG_DIR, 'mcp.log');\nconst logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });\nfunction log(message) {\n const timestamp = new Date().toISOString();\n logStream.write(timestamp + ' [MCP] ' + message + '\\n');\n}\n\n// Load fresh config on every call so walletHash/backendUrl never go stale\nfunction loadConfig() {\n try {\n return JSON.parse(fs.readFileSync(path.join(CONFIG_DIR, 'config.json'), 'utf8'));\n } catch (e) {\n return {};\n }\n}\n\nlet BACKEND_URL = DEFAULT_BACKEND;\nlet WALLET_HASH = '';\nfunction refreshConfig() {\n const cfg = loadConfig();\n BACKEND_URL = process.env.PAYTACA_BACKEND_URL || cfg.backendUrl || DEFAULT_BACKEND;\n WALLET_HASH = cfg.walletHash || '';\n}\n\n// Fetch a JSON payload over HTTP(S)\nfunction getJson(url, headers) {\n return new Promise((resolve, reject) => {\n let u;\n try {\n u = new URL(url);\n } catch (e) {\n return reject(new Error('Invalid URL: ' + url));\n }\n const requester = u.protocol === 'https:' ? https : http;\n const req = requester.get({\n hostname: u.hostname,\n port: u.port || (u.protocol === 'https:' ? 443 : 80),\n path: u.pathname + u.search,\n headers: headers || {},\n }, (res) => {\n let data = '';\n res.on('data', (chunk) => { data += chunk; });\n res.on('end', () => {\n if (res.statusCode >= 400) {\n reject(new Error('HTTP ' + res.statusCode + ': ' + data.substring(0, 200)));\n return;\n }\n try {\n resolve(JSON.parse(data));\n } catch (e) {\n reject(new Error('Invalid JSON response'));\n }\n });\n });\n req.on('error', reject);\n req.setTimeout(15000, () => {\n req.destroy();\n reject(new Error('Request timed out'));\n });\n });\n}\n\n// Run a shell command with a timeout\nfunction runCommand(cmd, args, timeoutMs) {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { shell: false });\n let stdout = '';\n let stderr = '';\n let settled = false;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n try { child.kill(); } catch (e) {}\n reject(new Error('Command timed out'));\n }, timeoutMs || 15000);\n child.stdout.on('data', (d) => { stdout += d.toString(); });\n child.stderr.on('data', (d) => { stderr += d.toString(); });\n child.on('close', (code) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n if (code === 0) resolve(stdout.trim());\n else reject(new Error(stderr.trim() || 'Command exited with code ' + code));\n });\n child.on('error', (err) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n reject(err);\n });\n });\n}\n\n// Format seconds as MM:SS or HH:MM:SS\nfunction formatDuration(totalSeconds) {\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n if (hours > 0) {\n return hours + ':' + String(minutes).padStart(2, '0') + ':' + String(secs).padStart(2, '0');\n }\n return minutes + ':' + String(secs).padStart(2, '0');\n}\n\n// Remaining time credits per model session\nasync function getCredits() {\n const data = await getJson(BACKEND_URL + '/v1/wallet/status', { 'X-Wallet-Hash': WALLET_HASH });\n const sessions = Array.isArray(data.sessions) ? data.sessions : [];\n const active = sessions.filter((s) => s.time_remaining_seconds > 0 && s.model_active);\n const inactive = sessions.filter((s) => s.time_remaining_seconds > 0 && !s.model_active);\n const parts = [];\n if (active.length > 0) {\n parts.push('Active time credits:');\n for (const s of active) {\n const total = formatDuration(s.time_credits_seconds);\n const remaining = formatDuration(s.time_remaining_seconds);\n const used = formatDuration(s.time_used_seconds);\n parts.push('- ' + (s.display_name || s.ai_model) + ': ' + remaining + ' remaining of ' + total + ' (' + used + ' used)');\n }\n }\n if (inactive.length > 0) {\n parts.push('');\n parts.push('Inactive models (credits but session not active):');\n for (const s of inactive) {\n parts.push('- ' + (s.display_name || s.ai_model) + ': ' + formatDuration(s.time_remaining_seconds) + ' remaining');\n }\n }\n if (parts.length === 0) {\n return 'No active time credits.';\n }\n return parts.join('\\n');\n}\n\n// Wallet BCH balance via the paytaca CLI\nasync function getBalance() {\n const out = await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n const match = out.match(/Balance:\\s*([\\d.]+)\\s*BCH/i);\n if (match) {\n return 'Wallet balance: ' + match[1] + ' BCH.';\n }\n return 'Could not parse balance. Raw output:\\n' + out.split('\\n').slice(0, 5).join('\\n');\n}\n\n// List all available models\nasync function getModels() {\n const data = await getJson(BACKEND_URL + '/v1/config', {});\n const models = Array.isArray(data.models) ? data.models : [];\n const lines = ['Available models:'];\n for (const m of models) {\n let line = m.id || 'unknown';\n if (m.display_name && m.display_name !== m.id) line += ' (' + m.display_name + ')';\n if (m.tier) line += ' [' + m.tier + ']';\n lines.push('- ' + line);\n }\n return lines.join('\\n');\n}\n\n// Plan pricing grouped by tier, optionally filtered to one model\nasync function getPlans(filterModel) {\n const data = await getJson(BACKEND_URL + '/v1/config', {});\n let models = Array.isArray(data.models) ? data.models : [];\n if (filterModel) {\n const f = String(filterModel).toLowerCase();\n models = models.filter((m) => {\n const id = String(m.id || '').toLowerCase();\n const name = String(m.display_name || '').toLowerCase();\n return id.indexOf(f) !== -1 || name.indexOf(f) !== -1;\n });\n }\n const groups = { budget: [], premium: [], frontier: [], other: [] };\n for (const m of models) {\n const key = String(m.tier || '').toLowerCase();\n const groupKey = (key === 'budget' || key === 'premium' || key === 'frontier') ? key : 'other';\n groups[groupKey].push(m);\n }\n const lines = ['Paytaca AI \u2014 Model Pricing'];\n const order = [\n { key: 'budget', label: 'Budget' },\n { key: 'premium', label: 'Premium' },\n { key: 'frontier', label: 'Frontier' },\n { key: 'other', label: 'Other' },\n ];\n let any = false;\n for (const g of order) {\n const group = groups[g.key];\n if (group.length === 0) continue;\n any = true;\n lines.push('');\n lines.push(g.label);\n for (const m of group) {\n lines.push('');\n const tiers = Array.isArray(m.price_tiers) ? m.price_tiers : [];\n if (tiers.length === 0) {\n lines.push('- ' + (m.display_name || m.id) + ': no pricing configured');\n continue;\n }\n const sorted = tiers.slice().sort((a, b) => (a.minutes || 0) - (b.minutes || 0));\n lines.push((m.display_name || m.id) + ':');\n for (const t of sorted) {\n const sats = typeof t.price_sats === 'number' ? t.price_sats : 0;\n const bch = (sats / 100000000).toFixed(8);\n const usd = typeof t.price_usd === 'number' ? t.price_usd.toFixed(4) : '?.??';\n lines.push(' ' + (t.minutes || 0) + ' minutes \u2014 USD ' + usd + ' (' + bch + ' BCH)');\n }\n }\n }\n if (!any) {\n lines.push('No models available.');\n }\n return lines.join('\\n');\n}\n\n// Recent wallet transactions via the paytaca CLI\nasync function getTransactions(args) {\n const cmdArgs = ['history'];\n if (args.type === 'incoming' || args.type === 'outgoing') {\n cmdArgs.push('--type', args.type);\n }\n const page = parseInt(args.page, 10);\n if (!isNaN(page) && page > 0) {\n cmdArgs.push('--page', String(page));\n }\n const out = await runCommand(PAYTACA_CMD, cmdArgs, 30000);\n return out || 'No transactions found.';\n}\n\n// Receiving address via the paytaca CLI (QR art suppressed)\nasync function getReceivingAddress(args) {\n const cmdArgs = ['receive', '--no-qr'];\n const amount = parseFloat(args.amount);\n if (!isNaN(amount) && amount > 0) {\n cmdArgs.push('--amount', String(amount));\n }\n const out = await runCommand(PAYTACA_CMD, cmdArgs, 30000);\n return out.trim() || 'Could not get receiving address.';\n}\n\n// CashToken holdings via the paytaca CLI (all tokens, or one category)\nasync function getTokens(args) {\n const category = args.category ? String(args.category).trim() : '';\n const cmdArgs = category ? ['token', 'info', category] : ['token', 'list'];\n const out = await runCommand(PAYTACA_CMD, cmdArgs, 30000);\n return out.trim() || (category ? 'Token not found: ' + category : 'No tokens found.');\n}\n\n// Send BCH or CashTokens. This spends real funds \u2014 opencode requires manual\n// user approval for this tool (permission 'paytaca_send' set to 'ask' by the\n// plugin's config hook), so it must never be called without the user asking\n// for the send.\nasync function sendFunds(args) {\n const address = String(args.address || '').trim();\n const amount = String(args.amount || '').trim();\n const unit = args.unit === 'sats' ? 'sats' : 'bch';\n const tokenCategory = args.token_category ? String(args.token_category).trim() : '';\n if (!address) {\n throw new Error('Missing recipient address.');\n }\n if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {\n throw new Error('Missing or invalid amount.');\n }\n const cmdArgs = tokenCategory\n ? ['token', 'send', address, amount, '--token', tokenCategory]\n : ['send', address, amount];\n if (!tokenCategory && unit === 'sats') {\n cmdArgs.push('--unit', 'sats');\n }\n log('Send requested: ' + cmdArgs.join(' '));\n const out = await runCommand(PAYTACA_CMD, cmdArgs, 90000);\n log('Send completed');\n return 'Transaction sent.\\n\\n' + out;\n}\n\n// Tool schemas (concise descriptions so MCP tool context stays small)\nconst TOOLS = [\n {\n name: 'get_credits',\n description: 'Get remaining Paytaca AI time credits (active model sessions, time left). Use when the user asks about credits, remaining time, session status, or how much usage they have left.',\n inputSchema: { type: 'object', properties: {} },\n },\n {\n name: 'get_balance',\n description: 'Get the BCH balance of the user\\'s Paytaca wallet. Use when the user asks about wallet balance or funds.',\n inputSchema: { type: 'object', properties: {} },\n },\n {\n name: 'get_models',\n description: 'List the AI models available on Paytaca AI (id, display name, tier). Use when the user asks which models are available.',\n inputSchema: { type: 'object', properties: {} },\n },\n {\n name: 'get_plans',\n description: 'Get Paytaca AI plan pricing: time tiers in minutes with USD and BCH prices, grouped by tier. Optionally pass a model id/name to filter one model. Use when the user asks about plans, pricing, costs, or how much a model costs.',\n inputSchema: {\n type: 'object',\n properties: {\n model: { type: 'string', description: 'Optional model id or display name to filter pricing to a single model.' },\n },\n },\n },\n {\n name: 'get_transactions',\n description: 'Get recent Paytaca wallet transactions (sent/received BCH). Use when the user asks about transaction history or latest transactions.',\n inputSchema: {\n type: 'object',\n properties: {\n type: { type: 'string', enum: ['incoming', 'outgoing'], description: 'Optional direction filter.' },\n page: { type: 'number', description: 'Optional 1-based page number for older history.' },\n },\n },\n },\n {\n name: 'get_receiving_address',\n description: 'Get a Paytaca wallet receiving address for depositing BCH, optionally as a BIP21 URI with an amount. Use when the user wants to fund the wallet or needs their address.',\n inputSchema: {\n type: 'object',\n properties: {\n amount: { type: 'number', description: 'Optional BCH amount to embed in a BIP21 payment URI.' },\n },\n },\n },\n {\n name: 'get_tokens',\n description: 'List CashToken holdings of the Paytaca wallet, or get details (name, symbol, balance, NFTs) for one token category. Use when the user asks about tokens or NFTs.',\n inputSchema: {\n type: 'object',\n properties: {\n category: { type: 'string', description: 'Optional token category id for details of a single token.' },\n },\n },\n },\n {\n name: 'send',\n description: 'Send BCH or CashTokens from the Paytaca wallet to an address. SPENDS REAL FUNDS \u2014 only call when the user explicitly asks to send; opencode will prompt the user for approval and that prompt must never be bypassed. Token amounts are in base units; recipients of tokens should use token-aware (z-prefix) addresses.',\n inputSchema: {\n type: 'object',\n required: ['address', 'amount'],\n properties: {\n address: { type: 'string', description: 'Recipient CashAddr (e.g. bitcoincash:qp...).' },\n amount: { type: 'string', description: 'Amount to send.' },\n unit: { type: 'string', enum: ['bch', 'sats'], description: 'Amount unit, default bch. Ignored for token sends.' },\n token_category: { type: 'string', description: 'Token category id to send CashTokens instead of BCH.' },\n },\n },\n },\n];\n\n// JSON-RPC over stdio (newline-delimited)\nlet buffer = '';\nfunction handleMessage(msg) {\n if (msg.method === 'initialize') {\n const requestedVersion = msg.params && msg.params.protocolVersion;\n send(msg.id, {\n protocolVersion: requestedVersion || PROTOCOL_VERSION,\n capabilities: { tools: {} },\n serverInfo: { name: 'paytaca', version: '1.1.0' },\n });\n return;\n }\n if (msg.method === 'notifications/initialized') {\n return;\n }\n if (msg.method === 'ping') {\n send(msg.id, {});\n return;\n }\n if (msg.method === 'tools/list') {\n send(msg.id, { tools: TOOLS });\n return;\n }\n if (msg.method === 'tools/call') {\n const name = msg.params && msg.params.name;\n const args = (msg.params && msg.params.arguments) || {};\n refreshConfig();\n (async () => {\n let text;\n try {\n switch (name) {\n case 'get_credits': text = await getCredits(); break;\n case 'get_balance': text = await getBalance(); break;\n case 'get_models': text = await getModels(); break;\n case 'get_plans': text = await getPlans(args.model); break;\n case 'get_transactions': text = await getTransactions(args); break;\n case 'get_receiving_address': text = await getReceivingAddress(args); break;\n case 'get_tokens': text = await getTokens(args); break;\n case 'send': text = await sendFunds(args); break;\n default: throw new Error('Unknown tool: ' + name);\n }\n send(msg.id, { content: [{ type: 'text', text: text }] });\n } catch (e) {\n log('Tool ' + name + ' failed: ' + e.message);\n send(msg.id, { content: [{ type: 'text', text: 'Error: ' + e.message }], isError: true });\n }\n })();\n return;\n }\n // Unknown request \u2014 respond with an empty result so the client never hangs\n if (typeof msg.id !== 'undefined' && msg.id !== null) {\n send(msg.id, {});\n }\n}\n\nfunction send(id, result) {\n const payload = { jsonrpc: '2.0', id, result };\n process.stdout.write(JSON.stringify(payload) + '\\n');\n}\n\nprocess.stdin.on('data', (chunk) => {\n buffer += chunk.toString();\n let idx;\n while ((idx = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.substring(0, idx).trim();\n buffer = buffer.substring(idx + 1);\n if (!line) continue;\n let msg;\n try {\n msg = JSON.parse(line);\n } catch (e) {\n continue;\n }\n try {\n handleMessage(msg);\n } catch (e) {\n log('handleMessage error: ' + e.message);\n }\n }\n});\n\nrefreshConfig();\nlog('MCP server started (backend=' + BACKEND_URL + ')');\n";
|
|
2
|
+
//# sourceMappingURL=mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/bundled/mcp.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,kBAAkB,y6hBA4c9B,CAAC"}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// This file contains the bundled MCP server script as a string
|
|
3
|
+
// It gets written to ~/.opencode-paytaca/mcp-server.js at runtime
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.MCP_SERVER_CONTENT = void 0;
|
|
6
|
+
exports.MCP_SERVER_CONTENT = `#!/usr/bin/env node
|
|
7
|
+
/**
|
|
8
|
+
* Paytaca MCP Server
|
|
9
|
+
*
|
|
10
|
+
* Registers Paytaca tools with opencode so the assistant can work with real
|
|
11
|
+
* data instead of guessing. Two groups:
|
|
12
|
+
* - Paytaca AI account (backend): credits, models, plan pricing
|
|
13
|
+
* - Paytaca wallet (paytaca CLI): balance, transactions, receiving address,
|
|
14
|
+
* token holdings, and sending funds
|
|
15
|
+
*
|
|
16
|
+
* The send tool moves real funds — opencode is configured (via the plugin's
|
|
17
|
+
* config hook) to require explicit user approval before it runs.
|
|
18
|
+
*
|
|
19
|
+
* Loaded automatically via the plugin's config hook (cfg.mcp['paytaca']).
|
|
20
|
+
* Uses only Node.js built-in modules.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const http = require('http');
|
|
24
|
+
const https = require('https');
|
|
25
|
+
const { spawn } = require('child_process');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
|
|
30
|
+
const CONFIG_DIR = process.env.PAYTACA_CONFIG_DIR || path.join(os.homedir(), '.opencode-paytaca');
|
|
31
|
+
const PAYTACA_CMD = process.env.PAYTACA_CMD || 'paytaca';
|
|
32
|
+
const DEFAULT_BACKEND = process.env.PAYTACA_BACKEND_URL || 'https://api.paytaca.ai';
|
|
33
|
+
|
|
34
|
+
const PROTOCOL_VERSION = '2025-06-18';
|
|
35
|
+
|
|
36
|
+
// Logging setup
|
|
37
|
+
const LOG_FILE = path.join(CONFIG_DIR, 'mcp.log');
|
|
38
|
+
const logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });
|
|
39
|
+
function log(message) {
|
|
40
|
+
const timestamp = new Date().toISOString();
|
|
41
|
+
logStream.write(timestamp + ' [MCP] ' + message + '\\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Load fresh config on every call so walletHash/backendUrl never go stale
|
|
45
|
+
function loadConfig() {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(path.join(CONFIG_DIR, 'config.json'), 'utf8'));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let BACKEND_URL = DEFAULT_BACKEND;
|
|
54
|
+
let WALLET_HASH = '';
|
|
55
|
+
function refreshConfig() {
|
|
56
|
+
const cfg = loadConfig();
|
|
57
|
+
BACKEND_URL = process.env.PAYTACA_BACKEND_URL || cfg.backendUrl || DEFAULT_BACKEND;
|
|
58
|
+
WALLET_HASH = cfg.walletHash || '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fetch a JSON payload over HTTP(S)
|
|
62
|
+
function getJson(url, headers) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
let u;
|
|
65
|
+
try {
|
|
66
|
+
u = new URL(url);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return reject(new Error('Invalid URL: ' + url));
|
|
69
|
+
}
|
|
70
|
+
const requester = u.protocol === 'https:' ? https : http;
|
|
71
|
+
const req = requester.get({
|
|
72
|
+
hostname: u.hostname,
|
|
73
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
74
|
+
path: u.pathname + u.search,
|
|
75
|
+
headers: headers || {},
|
|
76
|
+
}, (res) => {
|
|
77
|
+
let data = '';
|
|
78
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
79
|
+
res.on('end', () => {
|
|
80
|
+
if (res.statusCode >= 400) {
|
|
81
|
+
reject(new Error('HTTP ' + res.statusCode + ': ' + data.substring(0, 200)));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
resolve(JSON.parse(data));
|
|
86
|
+
} catch (e) {
|
|
87
|
+
reject(new Error('Invalid JSON response'));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
req.on('error', reject);
|
|
92
|
+
req.setTimeout(15000, () => {
|
|
93
|
+
req.destroy();
|
|
94
|
+
reject(new Error('Request timed out'));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Run a shell command with a timeout
|
|
100
|
+
function runCommand(cmd, args, timeoutMs) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const child = spawn(cmd, args, { shell: false });
|
|
103
|
+
let stdout = '';
|
|
104
|
+
let stderr = '';
|
|
105
|
+
let settled = false;
|
|
106
|
+
const timer = setTimeout(() => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
try { child.kill(); } catch (e) {}
|
|
110
|
+
reject(new Error('Command timed out'));
|
|
111
|
+
}, timeoutMs || 15000);
|
|
112
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
113
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
114
|
+
child.on('close', (code) => {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
settled = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
if (code === 0) resolve(stdout.trim());
|
|
119
|
+
else reject(new Error(stderr.trim() || 'Command exited with code ' + code));
|
|
120
|
+
});
|
|
121
|
+
child.on('error', (err) => {
|
|
122
|
+
if (settled) return;
|
|
123
|
+
settled = true;
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
reject(err);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Format seconds as MM:SS or HH:MM:SS
|
|
131
|
+
function formatDuration(totalSeconds) {
|
|
132
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
133
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
134
|
+
const secs = totalSeconds % 60;
|
|
135
|
+
if (hours > 0) {
|
|
136
|
+
return hours + ':' + String(minutes).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
|
|
137
|
+
}
|
|
138
|
+
return minutes + ':' + String(secs).padStart(2, '0');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Remaining time credits per model session
|
|
142
|
+
async function getCredits() {
|
|
143
|
+
const data = await getJson(BACKEND_URL + '/v1/wallet/status', { 'X-Wallet-Hash': WALLET_HASH });
|
|
144
|
+
const sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
|
145
|
+
const active = sessions.filter((s) => s.time_remaining_seconds > 0 && s.model_active);
|
|
146
|
+
const inactive = sessions.filter((s) => s.time_remaining_seconds > 0 && !s.model_active);
|
|
147
|
+
const parts = [];
|
|
148
|
+
if (active.length > 0) {
|
|
149
|
+
parts.push('Active time credits:');
|
|
150
|
+
for (const s of active) {
|
|
151
|
+
const total = formatDuration(s.time_credits_seconds);
|
|
152
|
+
const remaining = formatDuration(s.time_remaining_seconds);
|
|
153
|
+
const used = formatDuration(s.time_used_seconds);
|
|
154
|
+
parts.push('- ' + (s.display_name || s.ai_model) + ': ' + remaining + ' remaining of ' + total + ' (' + used + ' used)');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (inactive.length > 0) {
|
|
158
|
+
parts.push('');
|
|
159
|
+
parts.push('Inactive models (credits but session not active):');
|
|
160
|
+
for (const s of inactive) {
|
|
161
|
+
parts.push('- ' + (s.display_name || s.ai_model) + ': ' + formatDuration(s.time_remaining_seconds) + ' remaining');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (parts.length === 0) {
|
|
165
|
+
return 'No active time credits.';
|
|
166
|
+
}
|
|
167
|
+
return parts.join('\\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Wallet BCH balance via the paytaca CLI
|
|
171
|
+
async function getBalance() {
|
|
172
|
+
const out = await runCommand(PAYTACA_CMD, ['wallet', 'info']);
|
|
173
|
+
const match = out.match(/Balance:\\s*([\\d.]+)\\s*BCH/i);
|
|
174
|
+
if (match) {
|
|
175
|
+
return 'Wallet balance: ' + match[1] + ' BCH.';
|
|
176
|
+
}
|
|
177
|
+
return 'Could not parse balance. Raw output:\\n' + out.split('\\n').slice(0, 5).join('\\n');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// List all available models
|
|
181
|
+
async function getModels() {
|
|
182
|
+
const data = await getJson(BACKEND_URL + '/v1/config', {});
|
|
183
|
+
const models = Array.isArray(data.models) ? data.models : [];
|
|
184
|
+
const lines = ['Available models:'];
|
|
185
|
+
for (const m of models) {
|
|
186
|
+
let line = m.id || 'unknown';
|
|
187
|
+
if (m.display_name && m.display_name !== m.id) line += ' (' + m.display_name + ')';
|
|
188
|
+
if (m.tier) line += ' [' + m.tier + ']';
|
|
189
|
+
lines.push('- ' + line);
|
|
190
|
+
}
|
|
191
|
+
return lines.join('\\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Plan pricing grouped by tier, optionally filtered to one model
|
|
195
|
+
async function getPlans(filterModel) {
|
|
196
|
+
const data = await getJson(BACKEND_URL + '/v1/config', {});
|
|
197
|
+
let models = Array.isArray(data.models) ? data.models : [];
|
|
198
|
+
if (filterModel) {
|
|
199
|
+
const f = String(filterModel).toLowerCase();
|
|
200
|
+
models = models.filter((m) => {
|
|
201
|
+
const id = String(m.id || '').toLowerCase();
|
|
202
|
+
const name = String(m.display_name || '').toLowerCase();
|
|
203
|
+
return id.indexOf(f) !== -1 || name.indexOf(f) !== -1;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const groups = { budget: [], premium: [], frontier: [], other: [] };
|
|
207
|
+
for (const m of models) {
|
|
208
|
+
const key = String(m.tier || '').toLowerCase();
|
|
209
|
+
const groupKey = (key === 'budget' || key === 'premium' || key === 'frontier') ? key : 'other';
|
|
210
|
+
groups[groupKey].push(m);
|
|
211
|
+
}
|
|
212
|
+
const lines = ['Paytaca AI — Model Pricing'];
|
|
213
|
+
const order = [
|
|
214
|
+
{ key: 'budget', label: 'Budget' },
|
|
215
|
+
{ key: 'premium', label: 'Premium' },
|
|
216
|
+
{ key: 'frontier', label: 'Frontier' },
|
|
217
|
+
{ key: 'other', label: 'Other' },
|
|
218
|
+
];
|
|
219
|
+
let any = false;
|
|
220
|
+
for (const g of order) {
|
|
221
|
+
const group = groups[g.key];
|
|
222
|
+
if (group.length === 0) continue;
|
|
223
|
+
any = true;
|
|
224
|
+
lines.push('');
|
|
225
|
+
lines.push(g.label);
|
|
226
|
+
for (const m of group) {
|
|
227
|
+
lines.push('');
|
|
228
|
+
const tiers = Array.isArray(m.price_tiers) ? m.price_tiers : [];
|
|
229
|
+
if (tiers.length === 0) {
|
|
230
|
+
lines.push('- ' + (m.display_name || m.id) + ': no pricing configured');
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const sorted = tiers.slice().sort((a, b) => (a.minutes || 0) - (b.minutes || 0));
|
|
234
|
+
lines.push((m.display_name || m.id) + ':');
|
|
235
|
+
for (const t of sorted) {
|
|
236
|
+
const sats = typeof t.price_sats === 'number' ? t.price_sats : 0;
|
|
237
|
+
const bch = (sats / 100000000).toFixed(8);
|
|
238
|
+
const usd = typeof t.price_usd === 'number' ? t.price_usd.toFixed(4) : '?.??';
|
|
239
|
+
lines.push(' ' + (t.minutes || 0) + ' minutes — USD ' + usd + ' (' + bch + ' BCH)');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!any) {
|
|
244
|
+
lines.push('No models available.');
|
|
245
|
+
}
|
|
246
|
+
return lines.join('\\n');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Recent wallet transactions via the paytaca CLI
|
|
250
|
+
async function getTransactions(args) {
|
|
251
|
+
const cmdArgs = ['history'];
|
|
252
|
+
if (args.type === 'incoming' || args.type === 'outgoing') {
|
|
253
|
+
cmdArgs.push('--type', args.type);
|
|
254
|
+
}
|
|
255
|
+
const page = parseInt(args.page, 10);
|
|
256
|
+
if (!isNaN(page) && page > 0) {
|
|
257
|
+
cmdArgs.push('--page', String(page));
|
|
258
|
+
}
|
|
259
|
+
const out = await runCommand(PAYTACA_CMD, cmdArgs, 30000);
|
|
260
|
+
return out || 'No transactions found.';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Receiving address via the paytaca CLI (QR art suppressed)
|
|
264
|
+
async function getReceivingAddress(args) {
|
|
265
|
+
const cmdArgs = ['receive', '--no-qr'];
|
|
266
|
+
const amount = parseFloat(args.amount);
|
|
267
|
+
if (!isNaN(amount) && amount > 0) {
|
|
268
|
+
cmdArgs.push('--amount', String(amount));
|
|
269
|
+
}
|
|
270
|
+
const out = await runCommand(PAYTACA_CMD, cmdArgs, 30000);
|
|
271
|
+
return out.trim() || 'Could not get receiving address.';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// CashToken holdings via the paytaca CLI (all tokens, or one category)
|
|
275
|
+
async function getTokens(args) {
|
|
276
|
+
const category = args.category ? String(args.category).trim() : '';
|
|
277
|
+
const cmdArgs = category ? ['token', 'info', category] : ['token', 'list'];
|
|
278
|
+
const out = await runCommand(PAYTACA_CMD, cmdArgs, 30000);
|
|
279
|
+
return out.trim() || (category ? 'Token not found: ' + category : 'No tokens found.');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Send BCH or CashTokens. This spends real funds — opencode requires manual
|
|
283
|
+
// user approval for this tool (permission 'paytaca_send' set to 'ask' by the
|
|
284
|
+
// plugin's config hook), so it must never be called without the user asking
|
|
285
|
+
// for the send.
|
|
286
|
+
async function sendFunds(args) {
|
|
287
|
+
const address = String(args.address || '').trim();
|
|
288
|
+
const amount = String(args.amount || '').trim();
|
|
289
|
+
const unit = args.unit === 'sats' ? 'sats' : 'bch';
|
|
290
|
+
const tokenCategory = args.token_category ? String(args.token_category).trim() : '';
|
|
291
|
+
if (!address) {
|
|
292
|
+
throw new Error('Missing recipient address.');
|
|
293
|
+
}
|
|
294
|
+
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
|
|
295
|
+
throw new Error('Missing or invalid amount.');
|
|
296
|
+
}
|
|
297
|
+
const cmdArgs = tokenCategory
|
|
298
|
+
? ['token', 'send', address, amount, '--token', tokenCategory]
|
|
299
|
+
: ['send', address, amount];
|
|
300
|
+
if (!tokenCategory && unit === 'sats') {
|
|
301
|
+
cmdArgs.push('--unit', 'sats');
|
|
302
|
+
}
|
|
303
|
+
log('Send requested: ' + cmdArgs.join(' '));
|
|
304
|
+
const out = await runCommand(PAYTACA_CMD, cmdArgs, 90000);
|
|
305
|
+
log('Send completed');
|
|
306
|
+
return 'Transaction sent.\\n\\n' + out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Tool schemas (concise descriptions so MCP tool context stays small)
|
|
310
|
+
const TOOLS = [
|
|
311
|
+
{
|
|
312
|
+
name: 'get_credits',
|
|
313
|
+
description: 'Get remaining Paytaca AI time credits (active model sessions, time left). Use when the user asks about credits, remaining time, session status, or how much usage they have left.',
|
|
314
|
+
inputSchema: { type: 'object', properties: {} },
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'get_balance',
|
|
318
|
+
description: 'Get the BCH balance of the user\\'s Paytaca wallet. Use when the user asks about wallet balance or funds.',
|
|
319
|
+
inputSchema: { type: 'object', properties: {} },
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'get_models',
|
|
323
|
+
description: 'List the AI models available on Paytaca AI (id, display name, tier). Use when the user asks which models are available.',
|
|
324
|
+
inputSchema: { type: 'object', properties: {} },
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'get_plans',
|
|
328
|
+
description: 'Get Paytaca AI plan pricing: time tiers in minutes with USD and BCH prices, grouped by tier. Optionally pass a model id/name to filter one model. Use when the user asks about plans, pricing, costs, or how much a model costs.',
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: 'object',
|
|
331
|
+
properties: {
|
|
332
|
+
model: { type: 'string', description: 'Optional model id or display name to filter pricing to a single model.' },
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'get_transactions',
|
|
338
|
+
description: 'Get recent Paytaca wallet transactions (sent/received BCH). Use when the user asks about transaction history or latest transactions.',
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
properties: {
|
|
342
|
+
type: { type: 'string', enum: ['incoming', 'outgoing'], description: 'Optional direction filter.' },
|
|
343
|
+
page: { type: 'number', description: 'Optional 1-based page number for older history.' },
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: 'get_receiving_address',
|
|
349
|
+
description: 'Get a Paytaca wallet receiving address for depositing BCH, optionally as a BIP21 URI with an amount. Use when the user wants to fund the wallet or needs their address.',
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: 'object',
|
|
352
|
+
properties: {
|
|
353
|
+
amount: { type: 'number', description: 'Optional BCH amount to embed in a BIP21 payment URI.' },
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: 'get_tokens',
|
|
359
|
+
description: 'List CashToken holdings of the Paytaca wallet, or get details (name, symbol, balance, NFTs) for one token category. Use when the user asks about tokens or NFTs.',
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: 'object',
|
|
362
|
+
properties: {
|
|
363
|
+
category: { type: 'string', description: 'Optional token category id for details of a single token.' },
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: 'send',
|
|
369
|
+
description: 'Send BCH or CashTokens from the Paytaca wallet to an address. SPENDS REAL FUNDS — only call when the user explicitly asks to send; opencode will prompt the user for approval and that prompt must never be bypassed. Token amounts are in base units; recipients of tokens should use token-aware (z-prefix) addresses.',
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
required: ['address', 'amount'],
|
|
373
|
+
properties: {
|
|
374
|
+
address: { type: 'string', description: 'Recipient CashAddr (e.g. bitcoincash:qp...).' },
|
|
375
|
+
amount: { type: 'string', description: 'Amount to send.' },
|
|
376
|
+
unit: { type: 'string', enum: ['bch', 'sats'], description: 'Amount unit, default bch. Ignored for token sends.' },
|
|
377
|
+
token_category: { type: 'string', description: 'Token category id to send CashTokens instead of BCH.' },
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
// JSON-RPC over stdio (newline-delimited)
|
|
384
|
+
let buffer = '';
|
|
385
|
+
function handleMessage(msg) {
|
|
386
|
+
if (msg.method === 'initialize') {
|
|
387
|
+
const requestedVersion = msg.params && msg.params.protocolVersion;
|
|
388
|
+
send(msg.id, {
|
|
389
|
+
protocolVersion: requestedVersion || PROTOCOL_VERSION,
|
|
390
|
+
capabilities: { tools: {} },
|
|
391
|
+
serverInfo: { name: 'paytaca', version: '1.1.0' },
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (msg.method === 'notifications/initialized') {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (msg.method === 'ping') {
|
|
399
|
+
send(msg.id, {});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (msg.method === 'tools/list') {
|
|
403
|
+
send(msg.id, { tools: TOOLS });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (msg.method === 'tools/call') {
|
|
407
|
+
const name = msg.params && msg.params.name;
|
|
408
|
+
const args = (msg.params && msg.params.arguments) || {};
|
|
409
|
+
refreshConfig();
|
|
410
|
+
(async () => {
|
|
411
|
+
let text;
|
|
412
|
+
try {
|
|
413
|
+
switch (name) {
|
|
414
|
+
case 'get_credits': text = await getCredits(); break;
|
|
415
|
+
case 'get_balance': text = await getBalance(); break;
|
|
416
|
+
case 'get_models': text = await getModels(); break;
|
|
417
|
+
case 'get_plans': text = await getPlans(args.model); break;
|
|
418
|
+
case 'get_transactions': text = await getTransactions(args); break;
|
|
419
|
+
case 'get_receiving_address': text = await getReceivingAddress(args); break;
|
|
420
|
+
case 'get_tokens': text = await getTokens(args); break;
|
|
421
|
+
case 'send': text = await sendFunds(args); break;
|
|
422
|
+
default: throw new Error('Unknown tool: ' + name);
|
|
423
|
+
}
|
|
424
|
+
send(msg.id, { content: [{ type: 'text', text: text }] });
|
|
425
|
+
} catch (e) {
|
|
426
|
+
log('Tool ' + name + ' failed: ' + e.message);
|
|
427
|
+
send(msg.id, { content: [{ type: 'text', text: 'Error: ' + e.message }], isError: true });
|
|
428
|
+
}
|
|
429
|
+
})();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
// Unknown request — respond with an empty result so the client never hangs
|
|
433
|
+
if (typeof msg.id !== 'undefined' && msg.id !== null) {
|
|
434
|
+
send(msg.id, {});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function send(id, result) {
|
|
439
|
+
const payload = { jsonrpc: '2.0', id, result };
|
|
440
|
+
process.stdout.write(JSON.stringify(payload) + '\\n');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
process.stdin.on('data', (chunk) => {
|
|
444
|
+
buffer += chunk.toString();
|
|
445
|
+
let idx;
|
|
446
|
+
while ((idx = buffer.indexOf('\\n')) !== -1) {
|
|
447
|
+
const line = buffer.substring(0, idx).trim();
|
|
448
|
+
buffer = buffer.substring(idx + 1);
|
|
449
|
+
if (!line) continue;
|
|
450
|
+
let msg;
|
|
451
|
+
try {
|
|
452
|
+
msg = JSON.parse(line);
|
|
453
|
+
} catch (e) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
handleMessage(msg);
|
|
458
|
+
} catch (e) {
|
|
459
|
+
log('handleMessage error: ' + e.message);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
refreshConfig();
|
|
465
|
+
log('MCP server started (backend=' + BACKEND_URL + ')');
|
|
466
|
+
`;
|
|
467
|
+
//# sourceMappingURL=mcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/bundled/mcp.ts"],"names":[],"mappings":";AAAA,+DAA+D;AAC/D,kEAAkE;;;AAErD,QAAA,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4cjC,CAAC"}
|