@morebetterclaw/forge-swap 0.1.1

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.
@@ -0,0 +1,156 @@
1
+ # Deploying crypto-swap-agent to Railway.app
2
+
3
+ > Week 2 deployment guide — Railway.app setup, environment configuration, and monitoring.
4
+
5
+ ---
6
+
7
+ ## Prerequisites
8
+
9
+ - [Railway CLI](https://docs.railway.app/develop/cli) installed: `npm install -g @railway/cli`
10
+ - Railway account: [railway.app](https://railway.app)
11
+ - Project code pushed to GitHub or available locally
12
+
13
+ ---
14
+
15
+ ## Quick Deploy
16
+
17
+ ### Option A — GitHub (recommended)
18
+
19
+ 1. Go to [railway.app/new](https://railway.app/new)
20
+ 2. Select **"Deploy from GitHub repo"**
21
+ 3. Authorise Railway and choose the `crypto-swap-agent` repo
22
+ 4. Railway auto-detects Node.js and sets the build command
23
+ 5. Add environment variables (see table below)
24
+ 6. Click **Deploy**
25
+
26
+ ### Option B — Railway CLI
27
+
28
+ ```bash
29
+ # Install CLI
30
+ npm install -g @railway/cli
31
+
32
+ # Login
33
+ railway login
34
+
35
+ # Initialise project (from project root)
36
+ railway init
37
+
38
+ # Deploy
39
+ railway up
40
+
41
+ # Open service URL
42
+ railway open
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Environment Variables
48
+
49
+ Set these in Railway Dashboard → **Variables** tab, or via `railway variables set KEY=VALUE`.
50
+
51
+ | Variable | Required | Default | Description |
52
+ |---|---|---|---|
53
+ | `NODE_ENV` | Yes | `production` | Node environment |
54
+ | `PORT` | No | `3000` | Port (Railway injects this automatically) |
55
+ | `THORNODE_URL` | Yes | `https://thornode.ninerealms.com` | THORNode API endpoint |
56
+ | `LOG_LEVEL` | No | `info` | Logging verbosity (`debug`, `info`, `warn`, `error`) |
57
+ | `RATE_LIMIT_WINDOW_MS` | No | `60000` | Rate limit window in ms |
58
+ | `RATE_LIMIT_MAX` | No | `100` | Max requests per window |
59
+ | `ALLOWED_ORIGINS` | No | `*` | CORS allowed origins (comma-separated) |
60
+
61
+ > **Never set private keys or wallet secrets in Railway environment variables** unless using Railway's encrypted secrets feature. Prefer a secrets manager (e.g. Doppler, HashiCorp Vault) for sensitive values.
62
+
63
+ ---
64
+
65
+ ## Health Check Configuration
66
+
67
+ Railway uses the `/health` endpoint to determine service availability.
68
+
69
+ In Railway Dashboard → **Settings** → **Health Check**:
70
+
71
+ ```
72
+ Path: /health
73
+ Timeout: 10s
74
+ ```
75
+
76
+ The endpoint returns:
77
+ ```json
78
+ {
79
+ "status": "ok",
80
+ "version": "1.0.0",
81
+ "timestamp": "2025-03-09T23:00:00.000Z"
82
+ }
83
+ ```
84
+
85
+ Railway will restart the service if this endpoint returns a non-200 status.
86
+
87
+ ---
88
+
89
+ ## Custom Domain
90
+
91
+ 1. Dashboard → **Settings** → **Domains**
92
+ 2. Click **Generate Domain** for a `*.railway.app` subdomain, OR
93
+ 3. Click **Custom Domain** and add your CNAME record
94
+
95
+ ---
96
+
97
+ ## Monitoring
98
+
99
+ ### Railway Built-in Metrics
100
+
101
+ Railway Dashboard provides:
102
+ - CPU and memory usage graphs
103
+ - Request logs (stream via `railway logs --tail`)
104
+ - Deployment history and rollback
105
+
106
+ ### External Uptime Monitoring (recommended)
107
+
108
+ | Tool | Free tier | Setup |
109
+ |---|---|---|
110
+ | [UptimeRobot](https://uptimerobot.com) | 50 monitors, 5-min checks | Add `GET https://<your-domain>/health` |
111
+ | [Betterstack](https://betterstack.com/uptime) | 10 monitors, 3-min checks | Add HTTP monitor for `/health` |
112
+ | [Cronitor](https://cronitor.io) | 5 monitors | Monitor URL + alert on non-200 |
113
+
114
+ ### Log Streaming
115
+
116
+ ```bash
117
+ # Tail live logs
118
+ railway logs --tail
119
+
120
+ # Filter for errors
121
+ railway logs --tail | grep ERROR
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Scaling
127
+
128
+ Railway supports vertical scaling (memory/CPU) per service:
129
+
130
+ - Dashboard → **Settings** → **Resources**
131
+ - Recommended starting config: **512 MB RAM, 0.5 vCPU**
132
+ - Scale up if handling high quote volume or running background scan loops
133
+
134
+ ---
135
+
136
+ ## Rollback
137
+
138
+ ```bash
139
+ # List recent deployments
140
+ railway deployments list
141
+
142
+ # Roll back to a specific deployment
143
+ railway deployments rollback <deployment-id>
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Troubleshooting
149
+
150
+ | Issue | Solution |
151
+ |---|---|
152
+ | Build fails | Check `package.json` `engines.node` matches Railway's Node version |
153
+ | Health check failing | Verify `/health` route is mounted before auth middleware |
154
+ | Port not bound | Ensure `app.listen(process.env.PORT \|\| 3000)` — Railway injects `PORT` |
155
+ | Missing env vars | Check Railway Variables tab; redeploy after adding new vars |
156
+ | Memory OOM | Increase RAM in Railway Resources settings |
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@morebetterclaw/forge-swap",
3
+ "version": "0.1.1",
4
+ "description": "FORGE — cross-chain swap agent powered by THORChain. Protocol-native routing with embedded affiliate fees.",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "mcpName": "io.github.morebetterclaw/forge-swap",
8
+ "scripts": {
9
+ "start": "node src/index.js",
10
+ "dev": "node --watch src/index.js",
11
+ "telegram": "node src/telegram.js",
12
+ "test": "node src/test.js",
13
+ "test:api": "node src/test.js"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.27.1",
17
+ "dotenv": "^16.4.1",
18
+ "ethers": "^6.16.0",
19
+ "express": "^4.18.2",
20
+ "express-rate-limit": "^7.2.0"
21
+ },
22
+ "optionalDependencies": {
23
+ "node-telegram-bot-api": "^0.64.0"
24
+ },
25
+ "keywords": [
26
+ "openclaw",
27
+ "thorchain",
28
+ "swapkit",
29
+ "defi",
30
+ "swap",
31
+ "mcp",
32
+ "cross-chain"
33
+ ],
34
+ "author": "MoreBetter Studios",
35
+ "license": "SEE LICENSE IN LICENSE.md",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/morebetterclaw/forge.git"
39
+ }
40
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "FORGE",
3
+ "version": "0.1.0",
4
+ "description": "Cross-chain swap routing via THORChain. Non-custodial, protocol-native affiliate fee model.",
5
+ "url": "https://forge-api-production-50de.up.railway.app/mcp",
6
+ "transport": "streamable-http",
7
+ "tools": ["forge_quote", "forge_execute", "forge_assets", "forge_status"],
8
+ "contact": "https://morebetterstudios.com",
9
+ "license": "MIT"
10
+ }
package/railway.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://railway.app/railway.schema.json",
3
+ "build": {
4
+ "builder": "NIXPACKS"
5
+ },
6
+ "deploy": {
7
+ "startCommand": "node src/index.js",
8
+ "healthcheckPath": "/health",
9
+ "healthcheckTimeout": 30,
10
+ "restartPolicyType": "ON_FAILURE",
11
+ "restartPolicyMaxRetries": 3
12
+ }
13
+ }
package/server.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.morebetterclaw/forge-swap",
4
+ "description": "FORGE Swap — cross-chain swaps via THORChain. Quote and execute BTC, ETH, RUNE, USDC swaps.",
5
+ "repository": {
6
+ "url": "https://github.com/morebetterclaw/forge",
7
+ "source": "github"
8
+ },
9
+ "version": "0.1.1",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@morebetterclaw/forge-swap",
14
+ "version": "0.1.1",
15
+ "transport": {
16
+ "type": "streamable-http",
17
+ "url": "https://forge-api-production-50de.up.railway.app/mcp"
18
+ }
19
+ }
20
+ ]
21
+ }
package/setup-check.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * setup-check.js — Crypto Swap Agent pre-flight check
4
+ * Run: node setup-check.js
5
+ *
6
+ * Verifies all required env vars and connectivity before deployment.
7
+ */
8
+
9
+ import 'dotenv/config';
10
+ import https from 'https';
11
+
12
+ const RED = '\x1b[31m';
13
+ const GREEN = '\x1b[32m';
14
+ const YELLOW = '\x1b[33m';
15
+ const RESET = '\x1b[0m';
16
+
17
+ let pass = 0, warn = 0, fail = 0;
18
+
19
+ function check(label, condition, hint = '', severity = 'fail') {
20
+ if (condition) {
21
+ console.log(`${GREEN} ✓${RESET} ${label}`);
22
+ pass++;
23
+ } else {
24
+ const icon = severity === 'warn' ? `${YELLOW} ⚠${RESET}` : `${RED} ✗${RESET}`;
25
+ console.log(`${icon} ${label}`);
26
+ if (hint) console.log(` → ${hint}`);
27
+ severity === 'warn' ? warn++ : fail++;
28
+ }
29
+ }
30
+
31
+ async function ping(url, label) {
32
+ return new Promise(resolve => {
33
+ https.get(url, { timeout: 5000 }, res => {
34
+ check(`${label} reachable (HTTP ${res.statusCode})`, res.statusCode < 500);
35
+ resolve();
36
+ }).on('error', err => {
37
+ check(`${label} reachable`, false, err.message);
38
+ resolve();
39
+ });
40
+ });
41
+ }
42
+
43
+ (async () => {
44
+ console.log('\n⚡ Crypto Swap Agent — Setup Check\n');
45
+
46
+ // Env vars
47
+ console.log('─── Environment Variables ───────────────────────');
48
+ check('FEE_RECIPIENT_ADDRESS set',
49
+ !!process.env.FEE_RECIPIENT_ADDRESS && !process.env.FEE_RECIPIENT_ADDRESS.includes('your_address'),
50
+ 'Set a valid thor1... address to receive 0.5% affiliate fees');
51
+ check('SWAP_FEE_BPS set',
52
+ !!process.env.SWAP_FEE_BPS,
53
+ 'Default: 50 (= 0.5%). Add to .env', 'warn');
54
+ check('WALLET_PRIVATE_KEY set (for autonomous execution)',
55
+ !!process.env.WALLET_PRIVATE_KEY,
56
+ 'Optional for quote-only mode. Required for autonomous swap execution.', 'warn');
57
+ check('EVM_RPC_URL set',
58
+ !!process.env.EVM_RPC_URL && !process.env.EVM_RPC_URL.includes('your_key'),
59
+ 'Required for EVM swap execution. Get free key at alchemy.com', 'warn');
60
+ check('PORT set', !!process.env.PORT, 'Default: 3000', 'warn');
61
+
62
+ // Connectivity
63
+ console.log('\n─── Connectivity ─────────────────────────────────');
64
+ await ping('https://thornode.ninerealms.com/thorchain/inbound_addresses', 'THORChain NineRealms');
65
+ await ping('https://midgard.ninerealms.com/v2/pools?limit=1', 'THORChain Midgard');
66
+ await ping('https://api.coingecko.com/api/v3/ping', 'CoinGecko');
67
+
68
+ // Summary
69
+ console.log('\n─── Summary ──────────────────────────────────────');
70
+ console.log(` ${GREEN}${pass} passed${RESET} ${YELLOW}${warn} warnings${RESET} ${RED}${fail} failed${RESET}`);
71
+ if (fail > 0) {
72
+ console.log(`\n${RED}⚠ Fix failed checks before deploying.${RESET}`);
73
+ process.exit(1);
74
+ } else if (warn > 0) {
75
+ console.log(`\n${YELLOW}⚠ Warnings present — check before going live.${RESET}`);
76
+ console.log('\nReady for: node src/test.js → then deploy to Railway');
77
+ } else {
78
+ console.log(`\n${GREEN}✓ All checks passed. Deploy with: railway up${RESET}`);
79
+ }
80
+ })();
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Output formatters — structured JSON responses for OpenClaw + agent consumers
3
+ */
4
+
5
+ export function formatQuote(quoteData, parsed) {
6
+ const routes = quoteData.routes || [];
7
+ if (!routes.length) {
8
+ return { success: false, error: 'No routes found', pair: `${parsed.fromAsset} → ${parsed.toAsset}` };
9
+ }
10
+
11
+ const best = routes[0];
12
+ return {
13
+ success: true,
14
+ pair: `${parsed.fromAsset} → ${parsed.toAsset}`,
15
+ sellAmount: parsed.amount,
16
+ expectedOutput: best.expectedOutputMaxSlippage,
17
+ expectedOutputOptimal: best.expectedOutput,
18
+ priceImpact: best.priceImpactPercent,
19
+ route: best.providers?.join(' → ') || 'THORChain',
20
+ estimatedTimeSeconds: best.estimatedTime,
21
+ affiliateFee: '0.5%',
22
+ warning: best.warnings?.[0] || null,
23
+ rawRoutes: routes.length,
24
+ timestamp: new Date().toISOString()
25
+ };
26
+ }
27
+
28
+ export function formatSwapResult(swapData) {
29
+ return {
30
+ success: true,
31
+ status: swapData.status,
32
+ memo: swapData.memo,
33
+ depositAddress: swapData.depositAddress,
34
+ depositAsset: swapData.depositAsset,
35
+ depositAmount: swapData.depositAmount,
36
+ expectedOutput: swapData.expectedOutput,
37
+ route: swapData.route,
38
+ affiliateFee: swapData.affiliateFee,
39
+ nextStep: swapData.warning,
40
+ timestamp: new Date().toISOString()
41
+ };
42
+ }
43
+
44
+ export function formatAssetList(assets) {
45
+ return {
46
+ success: true,
47
+ count: assets.length,
48
+ assets: assets.slice(0, 50).map(a => ({
49
+ identifier: a.identifier || a.asset,
50
+ chain: a.chain,
51
+ symbol: a.ticker || a.symbol,
52
+ decimals: a.decimals
53
+ })),
54
+ note: 'Showing top 50. Full list available via SwapKit API.'
55
+ };
56
+ }
package/src/health.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * health.js — Health check Express router
3
+ * Provides GET /health endpoint for Railway.app and uptime monitors.
4
+ */
5
+
6
+ import { Router } from 'express';
7
+
8
+ const router = Router();
9
+
10
+ const VERSION = process.env.npm_package_version || '1.0.0';
11
+
12
+ /**
13
+ * GET /health
14
+ * Returns service health status, version, and current timestamp.
15
+ * Used by Railway.app health checks and external uptime monitors.
16
+ *
17
+ * Response: { status: 'ok', version: '1.0.0', timestamp: '<ISO8601>' }
18
+ */
19
+ router.get('/health', (req, res) => {
20
+ res.status(200).json({
21
+ status: 'ok',
22
+ version: VERSION,
23
+ timestamp: new Date().toISOString(),
24
+ });
25
+ });
26
+
27
+ export default router;
package/src/index.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Crypto Swap Agent — MoreBetter Studios
3
+ * Cross-chain swap skill via THORChain direct integration
4
+ *
5
+ * Entry point:
6
+ * node src/index.js → starts HTTP server
7
+ * node src/index.js quote ... → CLI mode (local testing)
8
+ */
9
+
10
+ import 'dotenv/config';
11
+ import { SwapKitApi } from './swapkit.js';
12
+ import { parseSwapCommand } from './parser.js';
13
+ import { formatQuote, formatSwapResult, formatAssetList } from './formatter.js';
14
+
15
+ const FEE_BPS = parseInt(process.env.SWAP_FEE_BPS || '50');
16
+ const FEE_ADDRESS = process.env.FEE_RECIPIENT_ADDRESS || '';
17
+
18
+ const CLI_COMMANDS = ['quote', 'execute', 'status', 'assets', 'list'];
19
+ const firstArg = process.argv[2]?.toLowerCase();
20
+
21
+ if (firstArg && CLI_COMMANDS.includes(firstArg)) {
22
+ // ── CLI mode ──────────────────────────────────────────────────────────────
23
+ const api = new SwapKitApi({ feeBps: FEE_BPS, feeAddress: FEE_ADDRESS });
24
+ const command = process.argv.slice(2).join(' ');
25
+
26
+ async function main(command) {
27
+ const parsed = parseSwapCommand(command);
28
+
29
+ switch (parsed.action) {
30
+ case 'quote': {
31
+ const quote = await api.getQuote(parsed.fromAsset, parsed.toAsset, parsed.amount);
32
+ console.log(JSON.stringify(formatQuote(quote, parsed), null, 2));
33
+ break;
34
+ }
35
+ case 'execute': {
36
+ if (!parsed.destAddress) {
37
+ console.log(JSON.stringify({ error: 'Destination address required for execute' }));
38
+ process.exit(1);
39
+ }
40
+ const result = await api.executeSwap(parsed.fromAsset, parsed.toAsset, parsed.amount, parsed.destAddress);
41
+ console.log(JSON.stringify(formatSwapResult(result), null, 2));
42
+ break;
43
+ }
44
+ case 'status': {
45
+ const status = await api.getStatus(parsed.txHash);
46
+ console.log(JSON.stringify(status, null, 2));
47
+ break;
48
+ }
49
+ case 'assets': {
50
+ const assets = await api.getSupportedAssets();
51
+ console.log(JSON.stringify(formatAssetList(assets), null, 2));
52
+ break;
53
+ }
54
+ default:
55
+ console.log(JSON.stringify({
56
+ error: `Unknown command: ${parsed.action}`,
57
+ supported: ['quote', 'execute', 'status', 'assets'],
58
+ }));
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ main(command).catch(err => {
64
+ console.log(JSON.stringify({ error: err.message }));
65
+ process.exit(1);
66
+ });
67
+
68
+ } else {
69
+ // ── Server mode ───────────────────────────────────────────────────────────
70
+ const { startServer } = await import('./server.js');
71
+ startServer();
72
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * mcp.js — Model Context Protocol server for FORGE
3
+ * Exposes 4 tools via Streamable HTTP transport (2026 spec).
4
+ */
5
+
6
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
9
+ import { Router } from 'express';
10
+
11
+ const TOOLS = [
12
+ {
13
+ name: 'forge_quote',
14
+ description:
15
+ 'Get a cross-chain swap quote via THORChain. Returns expected output, fees, route, and slippage. Use before executing any swap.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ fromAsset: {
20
+ type: 'string',
21
+ description: 'Source asset in CHAIN.TICKER format e.g. ETH.ETH, BTC.BTC, AVAX.USDC',
22
+ },
23
+ toAsset: {
24
+ type: 'string',
25
+ description: 'Destination asset in CHAIN.TICKER format',
26
+ },
27
+ amount: {
28
+ type: 'string',
29
+ description: 'Amount to swap in human-readable units e.g. 0.1',
30
+ },
31
+ },
32
+ required: ['fromAsset', 'toAsset', 'amount'],
33
+ },
34
+ },
35
+ {
36
+ name: 'forge_execute',
37
+ description:
38
+ 'Build a cross-chain swap transaction. Returns the vault deposit address and memo — the user\'s wallet sends funds to this address with the memo to execute the swap. FORGE never holds funds.',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ fromAsset: { type: 'string' },
43
+ toAsset: { type: 'string' },
44
+ amount: { type: 'string' },
45
+ destinationAddress: {
46
+ type: 'string',
47
+ description: 'Recipient address for the destination asset',
48
+ },
49
+ },
50
+ required: ['fromAsset', 'toAsset', 'amount', 'destinationAddress'],
51
+ },
52
+ },
53
+ {
54
+ name: 'forge_assets',
55
+ description:
56
+ 'List all supported assets available for cross-chain swapping via FORGE/THORChain.',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {},
60
+ },
61
+ },
62
+ {
63
+ name: 'forge_status',
64
+ description: 'Check the status of a cross-chain swap by transaction hash.',
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ txHash: {
69
+ type: 'string',
70
+ description: 'Transaction hash to check',
71
+ },
72
+ },
73
+ required: ['txHash'],
74
+ },
75
+ },
76
+ ];
77
+
78
+ /**
79
+ * Create an Express Router that mounts an MCP server at /mcp.
80
+ * @param {import('./swapkit.js').SwapKitApi} swapApi
81
+ * @returns {import('express').Router}
82
+ */
83
+ export function createMcpRouter(swapApi) {
84
+ const router = Router();
85
+
86
+ // Build MCP server once; transports are per-request (stateless mode)
87
+ const server = new Server(
88
+ { name: 'forge', version: '0.1.0' },
89
+ { capabilities: { tools: {} } }
90
+ );
91
+
92
+ // List tools
93
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
94
+ return { tools: TOOLS };
95
+ });
96
+
97
+ // Call tool
98
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
99
+ const { name, arguments: args } = request.params;
100
+
101
+ try {
102
+ let result;
103
+
104
+ switch (name) {
105
+ case 'forge_quote': {
106
+ const { fromAsset, toAsset, amount } = args;
107
+ result = await swapApi.getQuote(fromAsset, toAsset, parseFloat(amount));
108
+ break;
109
+ }
110
+ case 'forge_execute': {
111
+ const { fromAsset, toAsset, amount, destinationAddress } = args;
112
+ result = await swapApi.executeSwap(
113
+ fromAsset,
114
+ toAsset,
115
+ parseFloat(amount),
116
+ destinationAddress
117
+ );
118
+ break;
119
+ }
120
+ case 'forge_assets': {
121
+ result = await swapApi.getSupportedAssets();
122
+ break;
123
+ }
124
+ case 'forge_status': {
125
+ const { txHash } = args;
126
+ result = await swapApi.getStatus(txHash);
127
+ break;
128
+ }
129
+ default:
130
+ return {
131
+ content: [{ type: 'text', text: `Error: Unknown tool: ${name}` }],
132
+ isError: true,
133
+ };
134
+ }
135
+
136
+ return {
137
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
138
+ };
139
+ } catch (err) {
140
+ return {
141
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
142
+ isError: true,
143
+ };
144
+ }
145
+ });
146
+
147
+ // POST /mcp — fresh server + transport per request (stateless)
148
+ router.post('/', async (req, res) => {
149
+ try {
150
+ // Create a fresh server instance for each request — MCP transport is stateful
151
+ const reqServer = new Server(
152
+ { name: 'forge', version: '0.1.0' },
153
+ { capabilities: { tools: {} } }
154
+ );
155
+
156
+ // Register tools on the fresh instance
157
+ reqServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
158
+ reqServer.setRequestHandler(CallToolRequestSchema, async (request) => {
159
+ const { name, arguments: args } = request.params;
160
+ try {
161
+ let result;
162
+ switch (name) {
163
+ case 'forge_quote': {
164
+ const { fromAsset, toAsset, amount } = args;
165
+ result = await swapApi.getQuote(fromAsset, toAsset, parseFloat(amount));
166
+ break;
167
+ }
168
+ case 'forge_execute': {
169
+ const { fromAsset, toAsset, amount, destinationAddress } = args;
170
+ result = await swapApi.executeSwap(fromAsset, toAsset, parseFloat(amount), destinationAddress);
171
+ break;
172
+ }
173
+ case 'forge_assets': {
174
+ result = await swapApi.getSupportedAssets();
175
+ break;
176
+ }
177
+ case 'forge_status': {
178
+ const { txHash } = args;
179
+ result = await swapApi.getStatus(txHash);
180
+ break;
181
+ }
182
+ default:
183
+ return { content: [{ type: 'text', text: `Error: Unknown tool: ${name}` }], isError: true };
184
+ }
185
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
186
+ } catch (err) {
187
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
188
+ }
189
+ });
190
+
191
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
192
+ await reqServer.connect(transport);
193
+ await transport.handleRequest(req, res, req.body);
194
+ } catch (err) {
195
+ if (!res.headersSent) res.status(500).json({ error: err.message });
196
+ }
197
+ });
198
+
199
+ return router;
200
+ }