From f88b2e37e2ae9f1b54fc4aca0c6612733024a408 Mon Sep 17 00:00:00 2001 From: Ran Hammer Date: Mon, 23 Mar 2026 12:19:39 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=8C=20Add=20MCP=20server=20wrapper?= =?UTF-8?q?=20for=20advanced-swap-orders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a FastMCP-based MCP server that wraps the existing order.js skill as standard MCP tools, enabling registration in MCP directories (Official Registry, Smithery, LobeHub, etc.) Tools: - prepare_order: prepare gasless swap orders with EIP-712 typed data - submit_order: submit signed orders to relay network - query_orders: query order status by swapper or hash - get_supported_chains: return chain matrix from manifest.json - get_token_addressbook: return common token addresses per chain Supports stdio and HTTP transport via FastMCP. Zero changes to underlying order.js or contracts. Related: Orbs Agentic GTM V2 - MCP directory distribution strategy --- skills/advanced-swap-orders/MCP-README.md | 67 ++++++++ skills/advanced-swap-orders/mcp-server.py | 188 ++++++++++++++++++++++ skills/advanced-swap-orders/start-mcp.sh | 14 ++ 3 files changed, 269 insertions(+) create mode 100644 skills/advanced-swap-orders/MCP-README.md create mode 100755 skills/advanced-swap-orders/mcp-server.py create mode 100755 skills/advanced-swap-orders/start-mcp.sh diff --git a/skills/advanced-swap-orders/MCP-README.md b/skills/advanced-swap-orders/MCP-README.md new file mode 100644 index 0000000..a1eb84a --- /dev/null +++ b/skills/advanced-swap-orders/MCP-README.md @@ -0,0 +1,67 @@ +# MCP Server — Orbs Advanced Swap Orders + +Model Context Protocol (MCP) server exposing gasless, oracle-protected swap orders across 10 EVM chains. + +## Quick Start + +```bash +# stdio transport (for MCP clients like Claude Desktop, Cursor, etc.) +./start-mcp.sh + +# HTTP transport (for web-based MCP clients) +./start-mcp.sh http 8000 +``` + +## Tools + +| Tool | Description | +|------|-------------| +| `prepare_order` | Prepare a gasless swap order (market, limit, stop-loss, take-profit, TWAP, delayed-start). Returns EIP-712 typed data for signing. | +| `submit_order` | Submit a signed order to the Orbs relay network for oracle-protected execution. | +| `query_orders` | Query order status by swapper address or order hash. | +| `get_supported_chains` | List all supported chains with IDs, names, and adapter addresses. | +| `get_token_addressbook` | Common token addresses (WETH, USDC, USDT, etc.) for every supported chain. | + +## Workflow + +1. **`get_token_addressbook`** → find token addresses for your chain +2. **`prepare_order`** → get EIP-712 typed data + approval calldata +3. Sign the typed data with the user's wallet (off-chain, gasless) +4. **`submit_order`** → send signed order to Orbs relay network +5. **`query_orders`** → monitor order execution status + +## MCP Client Configuration + +### Claude Desktop / Cursor (`claude_desktop_config.json`) + +```json +{ + "mcpServers": { + "orbs-swap": { + "command": "/path/to/skills/advanced-swap-orders/start-mcp.sh" + } + } +} +``` + +### HTTP Mode (for remote clients) + +```json +{ + "mcpServers": { + "orbs-swap": { + "url": "http://localhost:8000/mcp" + } + } +} +``` + +## Supported Chains + +Ethereum (1), BNB Chain (56), Polygon (137), Sonic (146), Base (8453), Arbitrum One (42161), Avalanche (43114), Linea (59144) — plus Optimism (10) and Mantle (5000) via runtime config. + +## Requirements + +- Python 3.10+ +- `fastmcp` (`pip install fastmcp`) +- Node.js (for `scripts/order.js`) diff --git a/skills/advanced-swap-orders/mcp-server.py b/skills/advanced-swap-orders/mcp-server.py new file mode 100755 index 0000000..bc88ca1 --- /dev/null +++ b/skills/advanced-swap-orders/mcp-server.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +""" +Orbs Advanced Swap Orders — MCP Server + +Non-custodial, decentralized, gasless swap orders with oracle-protected execution. +Supports market, limit, stop-loss, take-profit, delayed-start, and chunked/TWAP-style +orders across 10 EVM chains via ownerless, immutable, audited, and verified contracts. + +Transport: stdio (default) or HTTP (--transport http --port 8000) +""" + +import json +import subprocess +import os +from fastmcp import FastMCP + +SKILL_DIR = os.path.dirname(os.path.abspath(__file__)) +ORDER_JS = os.path.join(SKILL_DIR, "scripts", "order.js") +MANIFEST_PATH = os.path.join(SKILL_DIR, "manifest.json") +TOKEN_ADDRESSBOOK_PATH = os.path.join(SKILL_DIR, "assets", "token-addressbook.md") + +# Load manifest once at startup +with open(MANIFEST_PATH, "r") as f: + MANIFEST = json.load(f) + +# Load token addressbook once at startup +with open(TOKEN_ADDRESSBOOK_PATH, "r") as f: + TOKEN_ADDRESSBOOK = f.read() + +CHAINS_INFO = MANIFEST.get("runtime", {}).get("chains", {}) +CHAIN_LIST = ", ".join( + f"{cid} ({info['name']})" for cid, info in sorted(CHAINS_INFO.items(), key=lambda x: int(x[0])) +) + +mcp = FastMCP( + "Orbs Advanced Swap Orders", + instructions=( + "Non-custodial, decentralized, gasless swap orders with oracle-protected execution " + "on every chunk. Supports market, limit, stop-loss, take-profit, delayed-start, and " + f"chunked/TWAP-style orders. Chains: {CHAIN_LIST}. " + "Uses ownerless, immutable, audited, battle-tested, and verified contracts. " + "Workflow: prepare_order → sign EIP-712 typed data off-chain → submit_order. " + "Query status with query_orders." + ), +) + + +def _run_order_js(args: list[str], stdin_data: str | None = None) -> str: + """Run node scripts/order.js with given args, optionally piping stdin.""" + cmd = ["node", ORDER_JS] + args + try: + result = subprocess.run( + cmd, + input=stdin_data, + capture_output=True, + text=True, + timeout=30, + cwd=SKILL_DIR, + ) + if result.returncode != 0: + error_msg = result.stderr.strip() or result.stdout.strip() or "Unknown error" + return json.dumps({"error": error_msg, "exitCode": result.returncode}) + return result.stdout.strip() + except subprocess.TimeoutExpired: + return json.dumps({"error": "Command timed out after 30 seconds"}) + except Exception as e: + return json.dumps({"error": str(e)}) + + +@mcp.tool +def prepare_order(params: str) -> str: + """Prepare a gasless, oracle-protected swap order for EIP-712 signing. + + Supports: market, limit, stop-loss, take-profit, delayed-start, and chunked/TWAP orders. + Returns EIP-712 typed data (for signing) and token approval calldata if needed. + + Args: + params: JSON string with order parameters. + Required fields: + - chainId: Chain ID (1=Ethereum, 56=BNB, 137=Polygon, 146=Sonic, + 8453=Base, 42161=Arbitrum, 43114=Avalanche, 59144=Linea) + - swapper: Ethereum address of the order creator + - input.token: Address of token to sell + - input.amount: Amount in wei (raw units) to sell + - output.token: Address of token to buy + Optional fields: + - output.limit: Minimum output amount (makes it a limit order) + - output.triggerLower: Lower price trigger (for stop-loss) + - output.triggerUpper: Upper price trigger (for take-profit) + - epoch: Seconds between chunks for TWAP/chunked orders + - start: Unix timestamp for delayed-start orders + - deadline: Order expiry as Unix timestamp + - slippage: Slippage in basis points (default 500 = 5%) + + Returns: + JSON with: typedData (EIP-712 for signing), approval calldata (if token + approval needed), order details, and any warnings. + """ + return _run_order_js(["prepare", "--params", "-"], stdin_data=params) + + +@mcp.tool +def submit_order(prepared: str, signature: str) -> str: + """Submit a signed order to the Orbs relay network for decentralized, oracle-protected execution. + + Call this after signing the EIP-712 typedData returned by prepare_order. + The order will be executed automatically by the Orbs network executors with + oracle price protection on every chunk. + + Args: + prepared: JSON string of the full prepared order (the complete output from prepare_order) + signature: EIP-712 signature as hex string (0x...) + + Returns: + JSON with submission result including order hash and status. + """ + return _run_order_js( + ["submit", "--prepared", "-", "--signature", signature], + stdin_data=prepared, + ) + + +@mcp.tool +def query_orders(swapper: str | None = None, order_hash: str | None = None) -> str: + """Query order status from the Orbs network. Provide either a swapper address or a specific order hash. + + Args: + swapper: Ethereum address of the order creator — returns all orders for this address + order_hash: Specific order hash (0x...) to query a single order + + Returns: + JSON with order status, fill history, and execution details. + """ + if not swapper and not order_hash: + return json.dumps({"error": "Provide either swapper address or order_hash"}) + args = ["query"] + if swapper: + args += ["--swapper", swapper] + if order_hash: + args += ["--hash", order_hash] + return _run_order_js(args) + + +@mcp.tool +def get_supported_chains() -> str: + """Get the list of supported EVM chains with chain IDs, names, and adapter contract addresses. + + Returns: + JSON object mapping chain ID to {name, adapter} for all supported chains. + """ + return json.dumps(CHAINS_INFO, indent=2) + + +@mcp.tool +def get_token_addressbook() -> str: + """Get common token addresses for all supported chains. + + Returns a markdown-formatted addressbook with token symbols and contract addresses + for popular tokens (WETH, WBTC, USDC, USDT, DAI, ORBS, etc.) on each chain. + Use these addresses as input.token and output.token in prepare_order. + + Returns: + Markdown text with token addresses grouped by chain. + """ + return TOKEN_ADDRESSBOOK + + +if __name__ == "__main__": + import sys + + transport = "stdio" + port = 8000 + args = sys.argv[1:] + i = 0 + while i < len(args): + if args[i] == "--transport" and i + 1 < len(args): + transport = args[i + 1] + i += 2 + elif args[i] == "--port" and i + 1 < len(args): + port = int(args[i + 1]) + i += 2 + else: + i += 1 + + if transport == "http": + mcp.run(transport="streamable-http", host="127.0.0.1", port=port) + else: + mcp.run(transport="stdio") diff --git a/skills/advanced-swap-orders/start-mcp.sh b/skills/advanced-swap-orders/start-mcp.sh new file mode 100755 index 0000000..87fe5f2 --- /dev/null +++ b/skills/advanced-swap-orders/start-mcp.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Start the Orbs Advanced Swap Orders MCP server +# Usage: +# ./start-mcp.sh # stdio transport (default, for MCP clients) +# ./start-mcp.sh http # HTTP transport on port 8000 +# ./start-mcp.sh http 9000 # HTTP transport on custom port + +set -euo pipefail +cd "$(dirname "$0")" + +TRANSPORT="${1:-stdio}" +PORT="${2:-8000}" + +exec python3 mcp-server.py --transport "$TRANSPORT" --port "$PORT" From 97b31cb3ec59c3cad5fe45f44360d462ff995bc0 Mon Sep 17 00:00:00 2001 From: Ran Hammer Date: Mon, 23 Mar 2026 13:06:42 +0000 Subject: [PATCH 2/4] refactor: Node.js MCP server, no Python dependency, reads from manifest - Single file mcp-server.js with zero npm dependencies - Implements MCP stdio protocol (JSON-RPC 2.0) inline - Reads chains, contracts, config from manifest.json at startup - Reads token addressbook from assets/token-addressbook.md - Tool descriptions dynamically include supported chain list - Removed Python mcp-server.py and fastmcp dependency --- skills/advanced-swap-orders/MCP-README.md | 36 ++- skills/advanced-swap-orders/mcp-server.js | 351 ++++++++++++++++++++++ skills/advanced-swap-orders/mcp-server.py | 188 ------------ skills/advanced-swap-orders/start-mcp.sh | 10 +- 4 files changed, 379 insertions(+), 206 deletions(-) create mode 100644 skills/advanced-swap-orders/mcp-server.js delete mode 100755 skills/advanced-swap-orders/mcp-server.py diff --git a/skills/advanced-swap-orders/MCP-README.md b/skills/advanced-swap-orders/MCP-README.md index a1eb84a..9841ccf 100644 --- a/skills/advanced-swap-orders/MCP-README.md +++ b/skills/advanced-swap-orders/MCP-README.md @@ -1,6 +1,8 @@ # MCP Server — Orbs Advanced Swap Orders -Model Context Protocol (MCP) server exposing gasless, oracle-protected swap orders across 10 EVM chains. +Model Context Protocol (MCP) server exposing gasless, oracle-protected swap orders across multiple EVM chains. + +**Zero external dependencies** — implements the MCP stdio protocol (JSON-RPC 2.0 over stdin/stdout) directly in Node.js. All configuration is read dynamically from `manifest.json` and `assets/`. ## Quick Start @@ -8,8 +10,8 @@ Model Context Protocol (MCP) server exposing gasless, oracle-protected swap orde # stdio transport (for MCP clients like Claude Desktop, Cursor, etc.) ./start-mcp.sh -# HTTP transport (for web-based MCP clients) -./start-mcp.sh http 8000 +# or directly +node mcp-server.js ``` ## Tools @@ -19,7 +21,7 @@ Model Context Protocol (MCP) server exposing gasless, oracle-protected swap orde | `prepare_order` | Prepare a gasless swap order (market, limit, stop-loss, take-profit, TWAP, delayed-start). Returns EIP-712 typed data for signing. | | `submit_order` | Submit a signed order to the Orbs relay network for oracle-protected execution. | | `query_orders` | Query order status by swapper address or order hash. | -| `get_supported_chains` | List all supported chains with IDs, names, and adapter addresses. | +| `get_supported_chains` | List all supported chains with IDs, names, and adapter addresses (from manifest.json). | | `get_token_addressbook` | Common token addresses (WETH, USDC, USDT, etc.) for every supported chain. | ## Workflow @@ -38,30 +40,42 @@ Model Context Protocol (MCP) server exposing gasless, oracle-protected swap orde { "mcpServers": { "orbs-swap": { - "command": "/path/to/skills/advanced-swap-orders/start-mcp.sh" + "command": "node", + "args": ["/path/to/skills/advanced-swap-orders/mcp-server.js"] } } } ``` -### HTTP Mode (for remote clients) +### Via npx (if published) ```json { "mcpServers": { "orbs-swap": { - "url": "http://localhost:8000/mcp" + "command": "npx", + "args": ["@orbs-network/spot", "mcp"] } } } ``` +## Architecture + +- **Single file**: `mcp-server.js` — no build step, no transpilation +- **Zero dependencies**: implements JSON-RPC 2.0 / MCP protocol inline +- **Config from manifest**: chains, contracts, and metadata read from `manifest.json` at startup +- **Token data from assets**: `assets/token-addressbook.md` loaded and served directly +- **Tool execution**: each tool calls `node scripts/order.js` via `child_process` +- **Transport**: stdio (newline-delimited JSON-RPC 2.0) + ## Supported Chains -Ethereum (1), BNB Chain (56), Polygon (137), Sonic (146), Base (8453), Arbitrum One (42161), Avalanche (43114), Linea (59144) — plus Optimism (10) and Mantle (5000) via runtime config. +Dynamically loaded from `manifest.json`. Currently: + +Ethereum (1), BNB Chain (56), Polygon (137), Sonic (146), Base (8453), Arbitrum One (42161), Avalanche (43114), Linea (59144). ## Requirements -- Python 3.10+ -- `fastmcp` (`pip install fastmcp`) -- Node.js (for `scripts/order.js`) +- Node.js 18+ (uses `node:fs`, `node:path`, `node:child_process`) +- No additional npm packages required diff --git a/skills/advanced-swap-orders/mcp-server.js b/skills/advanced-swap-orders/mcp-server.js new file mode 100644 index 0000000..886376a --- /dev/null +++ b/skills/advanced-swap-orders/mcp-server.js @@ -0,0 +1,351 @@ +#!/usr/bin/env node +'use strict'; + +/** + * Orbs Advanced Swap Orders — MCP Server (Node.js, zero dependencies) + * + * Implements the Model Context Protocol over stdio transport using + * JSON-RPC 2.0. No external dependencies — reads all config from + * manifest.json and assets/ at startup. + * + * Usage: + * node mcp-server.js + * npx @orbs-network/spot mcp + */ + +const fs = require('node:fs'); +const path = require('node:path'); +const { execFile } = require('node:child_process'); + +// ── Paths (relative to this file) ────────────────────────────────────────── +const SKILL_DIR = __dirname; +const MANIFEST_PATH = path.join(SKILL_DIR, 'manifest.json'); +const TOKEN_ADDRESSBOOK_PATH = path.join(SKILL_DIR, 'assets', 'token-addressbook.md'); +const ORDER_JS = path.join(SKILL_DIR, 'scripts', 'order.js'); + +// ── Load manifest & assets at startup ────────────────────────────────────── +const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')); +const tokenAddressbook = fs.readFileSync(TOKEN_ADDRESSBOOK_PATH, 'utf8'); + +const chains = manifest.runtime?.chains ?? {}; +const contracts = manifest.runtime?.contracts ?? {}; +const chainList = Object.entries(chains) + .sort((a, b) => Number(a[0]) - Number(b[0])) + .map(([id, info]) => `${id} (${info.name})`) + .join(', '); + +const chainEnum = Object.keys(chains).sort((a, b) => Number(a) - Number(b)); + +// ── MCP Protocol Constants ───────────────────────────────────────────────── +const PROTOCOL_VERSION = '2024-11-05'; +const SERVER_INFO = { + name: 'orbs-advanced-swap-orders', + version: '1.0.0', +}; +const SERVER_CAPABILITIES = { + tools: {}, +}; + +// ── Tool Definitions (dynamically built from manifest) ───────────────────── +const TOOLS = [ + { + name: 'prepare_order', + description: + 'Prepare a gasless, oracle-protected swap order for EIP-712 signing. ' + + 'Supports: market, limit, stop-loss, take-profit, delayed-start, and chunked/TWAP orders. ' + + 'Returns EIP-712 typed data (for signing) and token approval calldata if needed. ' + + `Chains: ${chainList}.`, + inputSchema: { + type: 'object', + properties: { + params: { + type: 'object', + description: 'Order parameters object', + properties: { + chainId: { + type: 'string', + description: `Chain ID. Supported: ${chainList}`, + enum: chainEnum, + }, + swapper: { + type: 'string', + description: 'Ethereum address of the order creator', + }, + input: { + type: 'object', + description: 'Input (sell) token config', + properties: { + token: { type: 'string', description: 'Token address to sell' }, + amount: { type: 'string', description: 'Amount in wei (raw units)' }, + }, + required: ['token', 'amount'], + }, + output: { + type: 'object', + description: 'Output (buy) token config', + properties: { + token: { type: 'string', description: 'Token address to buy' }, + limit: { type: 'string', description: 'Minimum output amount (limit order)' }, + triggerLower: { type: 'string', description: 'Lower price trigger (stop-loss)' }, + triggerUpper: { type: 'string', description: 'Upper price trigger (take-profit)' }, + }, + required: ['token'], + }, + epoch: { type: 'number', description: 'Seconds between chunks (TWAP/chunked orders)' }, + start: { type: 'number', description: 'Unix timestamp for delayed-start orders' }, + deadline: { type: 'number', description: 'Order expiry as Unix timestamp' }, + slippage: { type: 'number', description: 'Slippage in basis points (default 500 = 5%)' }, + }, + required: ['chainId', 'swapper', 'input', 'output'], + }, + }, + required: ['params'], + }, + }, + { + name: 'submit_order', + description: + 'Submit a signed order to the Orbs relay network for decentralized, oracle-protected execution. ' + + 'Call after signing the EIP-712 typedData returned by prepare_order.', + inputSchema: { + type: 'object', + properties: { + prepared: { + type: 'object', + description: 'Full prepared order object (complete output from prepare_order)', + }, + signature: { + type: 'string', + description: 'EIP-712 signature as hex string (0x...)', + }, + }, + required: ['prepared', 'signature'], + }, + }, + { + name: 'query_orders', + description: + 'Query order status from the Orbs network. Provide either a swapper address or a specific order hash.', + inputSchema: { + type: 'object', + properties: { + swapper: { + type: 'string', + description: 'Ethereum address — returns all orders for this address', + }, + order_hash: { + type: 'string', + description: 'Specific order hash (0x...) to query a single order', + }, + }, + }, + }, + { + name: 'get_supported_chains', + description: + 'Get the list of supported EVM chains with chain IDs, names, and adapter contract addresses. ' + + `Currently supported: ${chainList}.`, + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'get_token_addressbook', + description: + 'Get common token addresses (WETH, WBTC, USDC, USDT, DAI, ORBS, etc.) for all supported chains. ' + + 'Use these addresses as input.token and output.token in prepare_order.', + inputSchema: { + type: 'object', + properties: {}, + }, + }, +]; + +// ── Helper: run node scripts/order.js ────────────────────────────────────── +function runOrderJs(args, stdinData) { + return new Promise((resolve) => { + const child = execFile('node', [ORDER_JS, ...args], { + cwd: SKILL_DIR, + timeout: 30000, + maxBuffer: 1024 * 1024, + }, (error, stdout, stderr) => { + if (error) { + const msg = stderr?.trim() || stdout?.trim() || error.message || 'Unknown error'; + resolve(JSON.stringify({ error: msg, exitCode: error.code })); + } else { + resolve(stdout.trim()); + } + }); + + if (stdinData != null) { + child.stdin.write(typeof stdinData === 'string' ? stdinData : JSON.stringify(stdinData)); + child.stdin.end(); + } + }); +} + +// ── Tool Handlers ────────────────────────────────────────────────────────── +const toolHandlers = { + async prepare_order({ params }) { + const result = await runOrderJs( + ['prepare', '--params', '-'], + typeof params === 'string' ? params : JSON.stringify(params) + ); + return [{ type: 'text', text: result }]; + }, + + async submit_order({ prepared, signature }) { + const result = await runOrderJs( + ['submit', '--prepared', '-', '--signature', signature], + typeof prepared === 'string' ? prepared : JSON.stringify(prepared) + ); + return [{ type: 'text', text: result }]; + }, + + async query_orders({ swapper, order_hash }) { + if (!swapper && !order_hash) { + return [{ type: 'text', text: JSON.stringify({ error: 'Provide either swapper address or order_hash' }) }]; + } + const args = ['query']; + if (swapper) args.push('--swapper', swapper); + if (order_hash) args.push('--hash', order_hash); + const result = await runOrderJs(args); + return [{ type: 'text', text: result }]; + }, + + async get_supported_chains() { + return [{ type: 'text', text: JSON.stringify(chains, null, 2) }]; + }, + + async get_token_addressbook() { + return [{ type: 'text', text: tokenAddressbook }]; + }, +}; + +// ── JSON-RPC / MCP Message Handling ──────────────────────────────────────── +async function handleMessage(msg) { + const { jsonrpc, id, method, params } = msg; + + // Notifications (no id) — acknowledge silently + if (id === undefined || id === null) { + return null; // no response for notifications + } + + switch (method) { + case 'initialize': + return { + jsonrpc: '2.0', + id, + result: { + protocolVersion: PROTOCOL_VERSION, + capabilities: SERVER_CAPABILITIES, + serverInfo: SERVER_INFO, + instructions: + 'Non-custodial, decentralized, gasless swap orders with oracle-protected execution. ' + + `Workflow: prepare_order → sign EIP-712 → submit_order. Chains: ${chainList}.`, + }, + }; + + case 'tools/list': + return { + jsonrpc: '2.0', + id, + result: { tools: TOOLS }, + }; + + case 'tools/call': { + const toolName = params?.name; + const toolArgs = params?.arguments ?? {}; + const handler = toolHandlers[toolName]; + + if (!handler) { + return { + jsonrpc: '2.0', + id, + result: { + content: [{ type: 'text', text: `Unknown tool: ${toolName}` }], + isError: true, + }, + }; + } + + try { + const content = await handler(toolArgs); + return { + jsonrpc: '2.0', + id, + result: { content }, + }; + } catch (err) { + return { + jsonrpc: '2.0', + id, + result: { + content: [{ type: 'text', text: `Error: ${err.message}` }], + isError: true, + }, + }; + } + } + + case 'ping': + return { jsonrpc: '2.0', id, result: {} }; + + default: + return { + jsonrpc: '2.0', + id, + error: { code: -32601, message: `Method not found: ${method}` }, + }; + } +} + +// ── stdio Transport ──────────────────────────────────────────────────────── +function startStdioTransport() { + let buffer = ''; + + process.stdin.setEncoding('utf8'); + process.stdin.on('data', async (chunk) => { + buffer += chunk; + + // MCP uses newline-delimited JSON + let newlineIdx; + while ((newlineIdx = buffer.indexOf('\n')) !== -1) { + const line = buffer.slice(0, newlineIdx).trim(); + buffer = buffer.slice(newlineIdx + 1); + + if (!line) continue; + + let msg; + try { + msg = JSON.parse(line); + } catch { + const errResp = { + jsonrpc: '2.0', + id: null, + error: { code: -32700, message: 'Parse error' }, + }; + process.stdout.write(JSON.stringify(errResp) + '\n'); + continue; + } + + const response = await handleMessage(msg); + if (response) { + process.stdout.write(JSON.stringify(response) + '\n'); + } + } + }); + + process.stdin.on('end', () => { + process.exit(0); + }); + + // Prevent unhandled errors from crashing + process.on('uncaughtException', (err) => { + process.stderr.write(`MCP server error: ${err.message}\n`); + }); +} + +// ── Main ─────────────────────────────────────────────────────────────────── +startStdioTransport(); diff --git a/skills/advanced-swap-orders/mcp-server.py b/skills/advanced-swap-orders/mcp-server.py deleted file mode 100755 index bc88ca1..0000000 --- a/skills/advanced-swap-orders/mcp-server.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -""" -Orbs Advanced Swap Orders — MCP Server - -Non-custodial, decentralized, gasless swap orders with oracle-protected execution. -Supports market, limit, stop-loss, take-profit, delayed-start, and chunked/TWAP-style -orders across 10 EVM chains via ownerless, immutable, audited, and verified contracts. - -Transport: stdio (default) or HTTP (--transport http --port 8000) -""" - -import json -import subprocess -import os -from fastmcp import FastMCP - -SKILL_DIR = os.path.dirname(os.path.abspath(__file__)) -ORDER_JS = os.path.join(SKILL_DIR, "scripts", "order.js") -MANIFEST_PATH = os.path.join(SKILL_DIR, "manifest.json") -TOKEN_ADDRESSBOOK_PATH = os.path.join(SKILL_DIR, "assets", "token-addressbook.md") - -# Load manifest once at startup -with open(MANIFEST_PATH, "r") as f: - MANIFEST = json.load(f) - -# Load token addressbook once at startup -with open(TOKEN_ADDRESSBOOK_PATH, "r") as f: - TOKEN_ADDRESSBOOK = f.read() - -CHAINS_INFO = MANIFEST.get("runtime", {}).get("chains", {}) -CHAIN_LIST = ", ".join( - f"{cid} ({info['name']})" for cid, info in sorted(CHAINS_INFO.items(), key=lambda x: int(x[0])) -) - -mcp = FastMCP( - "Orbs Advanced Swap Orders", - instructions=( - "Non-custodial, decentralized, gasless swap orders with oracle-protected execution " - "on every chunk. Supports market, limit, stop-loss, take-profit, delayed-start, and " - f"chunked/TWAP-style orders. Chains: {CHAIN_LIST}. " - "Uses ownerless, immutable, audited, battle-tested, and verified contracts. " - "Workflow: prepare_order → sign EIP-712 typed data off-chain → submit_order. " - "Query status with query_orders." - ), -) - - -def _run_order_js(args: list[str], stdin_data: str | None = None) -> str: - """Run node scripts/order.js with given args, optionally piping stdin.""" - cmd = ["node", ORDER_JS] + args - try: - result = subprocess.run( - cmd, - input=stdin_data, - capture_output=True, - text=True, - timeout=30, - cwd=SKILL_DIR, - ) - if result.returncode != 0: - error_msg = result.stderr.strip() or result.stdout.strip() or "Unknown error" - return json.dumps({"error": error_msg, "exitCode": result.returncode}) - return result.stdout.strip() - except subprocess.TimeoutExpired: - return json.dumps({"error": "Command timed out after 30 seconds"}) - except Exception as e: - return json.dumps({"error": str(e)}) - - -@mcp.tool -def prepare_order(params: str) -> str: - """Prepare a gasless, oracle-protected swap order for EIP-712 signing. - - Supports: market, limit, stop-loss, take-profit, delayed-start, and chunked/TWAP orders. - Returns EIP-712 typed data (for signing) and token approval calldata if needed. - - Args: - params: JSON string with order parameters. - Required fields: - - chainId: Chain ID (1=Ethereum, 56=BNB, 137=Polygon, 146=Sonic, - 8453=Base, 42161=Arbitrum, 43114=Avalanche, 59144=Linea) - - swapper: Ethereum address of the order creator - - input.token: Address of token to sell - - input.amount: Amount in wei (raw units) to sell - - output.token: Address of token to buy - Optional fields: - - output.limit: Minimum output amount (makes it a limit order) - - output.triggerLower: Lower price trigger (for stop-loss) - - output.triggerUpper: Upper price trigger (for take-profit) - - epoch: Seconds between chunks for TWAP/chunked orders - - start: Unix timestamp for delayed-start orders - - deadline: Order expiry as Unix timestamp - - slippage: Slippage in basis points (default 500 = 5%) - - Returns: - JSON with: typedData (EIP-712 for signing), approval calldata (if token - approval needed), order details, and any warnings. - """ - return _run_order_js(["prepare", "--params", "-"], stdin_data=params) - - -@mcp.tool -def submit_order(prepared: str, signature: str) -> str: - """Submit a signed order to the Orbs relay network for decentralized, oracle-protected execution. - - Call this after signing the EIP-712 typedData returned by prepare_order. - The order will be executed automatically by the Orbs network executors with - oracle price protection on every chunk. - - Args: - prepared: JSON string of the full prepared order (the complete output from prepare_order) - signature: EIP-712 signature as hex string (0x...) - - Returns: - JSON with submission result including order hash and status. - """ - return _run_order_js( - ["submit", "--prepared", "-", "--signature", signature], - stdin_data=prepared, - ) - - -@mcp.tool -def query_orders(swapper: str | None = None, order_hash: str | None = None) -> str: - """Query order status from the Orbs network. Provide either a swapper address or a specific order hash. - - Args: - swapper: Ethereum address of the order creator — returns all orders for this address - order_hash: Specific order hash (0x...) to query a single order - - Returns: - JSON with order status, fill history, and execution details. - """ - if not swapper and not order_hash: - return json.dumps({"error": "Provide either swapper address or order_hash"}) - args = ["query"] - if swapper: - args += ["--swapper", swapper] - if order_hash: - args += ["--hash", order_hash] - return _run_order_js(args) - - -@mcp.tool -def get_supported_chains() -> str: - """Get the list of supported EVM chains with chain IDs, names, and adapter contract addresses. - - Returns: - JSON object mapping chain ID to {name, adapter} for all supported chains. - """ - return json.dumps(CHAINS_INFO, indent=2) - - -@mcp.tool -def get_token_addressbook() -> str: - """Get common token addresses for all supported chains. - - Returns a markdown-formatted addressbook with token symbols and contract addresses - for popular tokens (WETH, WBTC, USDC, USDT, DAI, ORBS, etc.) on each chain. - Use these addresses as input.token and output.token in prepare_order. - - Returns: - Markdown text with token addresses grouped by chain. - """ - return TOKEN_ADDRESSBOOK - - -if __name__ == "__main__": - import sys - - transport = "stdio" - port = 8000 - args = sys.argv[1:] - i = 0 - while i < len(args): - if args[i] == "--transport" and i + 1 < len(args): - transport = args[i + 1] - i += 2 - elif args[i] == "--port" and i + 1 < len(args): - port = int(args[i + 1]) - i += 2 - else: - i += 1 - - if transport == "http": - mcp.run(transport="streamable-http", host="127.0.0.1", port=port) - else: - mcp.run(transport="stdio") diff --git a/skills/advanced-swap-orders/start-mcp.sh b/skills/advanced-swap-orders/start-mcp.sh index 87fe5f2..e47b8ba 100755 --- a/skills/advanced-swap-orders/start-mcp.sh +++ b/skills/advanced-swap-orders/start-mcp.sh @@ -1,14 +1,10 @@ #!/usr/bin/env bash -# Start the Orbs Advanced Swap Orders MCP server +# Start the Orbs Advanced Swap Orders MCP server (stdio transport) # Usage: # ./start-mcp.sh # stdio transport (default, for MCP clients) -# ./start-mcp.sh http # HTTP transport on port 8000 -# ./start-mcp.sh http 9000 # HTTP transport on custom port +# node mcp-server.js # direct invocation set -euo pipefail cd "$(dirname "$0")" -TRANSPORT="${1:-stdio}" -PORT="${2:-8000}" - -exec python3 mcp-server.py --transport "$TRANSPORT" --port "$PORT" +exec node mcp-server.js From 3909f20a34d353197cb1f228dec11cde24e730d2 Mon Sep 17 00:00:00 2001 From: Ran Hammer Date: Mon, 23 Mar 2026 13:56:51 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20error=20handling,=20docs=20accuracy,=20cleanup=20st?= =?UTF-8?q?ale=20Python=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/advanced-swap-orders/mcp-server.js | 13 +++++++------ skills/advanced-swap-orders/start-mcp.sh | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/skills/advanced-swap-orders/mcp-server.js b/skills/advanced-swap-orders/mcp-server.js index 886376a..0546c36 100644 --- a/skills/advanced-swap-orders/mcp-server.js +++ b/skills/advanced-swap-orders/mcp-server.js @@ -43,7 +43,7 @@ const SERVER_INFO = { version: '1.0.0', }; const SERVER_CAPABILITIES = { - tools: {}, + tools: { listChanged: false }, }; // ── Tool Definitions (dynamically built from manifest) ───────────────────── @@ -164,7 +164,7 @@ const TOOLS = [ // ── Helper: run node scripts/order.js ────────────────────────────────────── function runOrderJs(args, stdinData) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const child = execFile('node', [ORDER_JS, ...args], { cwd: SKILL_DIR, timeout: 30000, @@ -172,7 +172,7 @@ function runOrderJs(args, stdinData) { }, (error, stdout, stderr) => { if (error) { const msg = stderr?.trim() || stdout?.trim() || error.message || 'Unknown error'; - resolve(JSON.stringify({ error: msg, exitCode: error.code })); + reject(new Error(msg)); } else { resolve(stdout.trim()); } @@ -205,7 +205,7 @@ const toolHandlers = { async query_orders({ swapper, order_hash }) { if (!swapper && !order_hash) { - return [{ type: 'text', text: JSON.stringify({ error: 'Provide either swapper address or order_hash' }) }]; + throw new Error('Provide either swapper address or order_hash'); } const args = ['query']; if (swapper) args.push('--swapper', swapper); @@ -227,8 +227,9 @@ const toolHandlers = { async function handleMessage(msg) { const { jsonrpc, id, method, params } = msg; - // Notifications (no id) — acknowledge silently - if (id === undefined || id === null) { + // Notifications (no id field) — acknowledge silently per JSON-RPC 2.0 spec + // Note: id === null is a valid request id; only missing id (undefined) is a notification + if (id === undefined) { return null; // no response for notifications } diff --git a/skills/advanced-swap-orders/start-mcp.sh b/skills/advanced-swap-orders/start-mcp.sh index e47b8ba..65a5617 100755 --- a/skills/advanced-swap-orders/start-mcp.sh +++ b/skills/advanced-swap-orders/start-mcp.sh @@ -5,6 +5,6 @@ # node mcp-server.js # direct invocation set -euo pipefail -cd "$(dirname "$0")" +cd -- "$(dirname -- "${BASH_SOURCE[0]}")" exec node mcp-server.js From 4c4e000a22066be138af1d9fb8405b22d95caa0d Mon Sep 17 00:00:00 2001 From: Ran Hammer Date: Mon, 23 Mar 2026 20:40:44 +0000 Subject: [PATCH 4/4] fix: message queuing, type guards, error handling per Copilot review --- skills/advanced-swap-orders/mcp-server.js | 55 ++++++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/skills/advanced-swap-orders/mcp-server.js b/skills/advanced-swap-orders/mcp-server.js index 0546c36..c63a48a 100644 --- a/skills/advanced-swap-orders/mcp-server.js +++ b/skills/advanced-swap-orders/mcp-server.js @@ -225,6 +225,15 @@ const toolHandlers = { // ── JSON-RPC / MCP Message Handling ──────────────────────────────────────── async function handleMessage(msg) { + // Type guard: msg must be a non-null object + if (typeof msg !== 'object' || msg === null) { + return { + jsonrpc: '2.0', + id: null, + error: { code: -32600, message: 'Invalid Request: expected a JSON object' }, + }; + } + const { jsonrpc, id, method, params } = msg; // Notifications (no id field) — acknowledge silently per JSON-RPC 2.0 spec @@ -305,9 +314,35 @@ async function handleMessage(msg) { // ── stdio Transport ──────────────────────────────────────────────────────── function startStdioTransport() { let buffer = ''; + const messageQueue = []; + let processing = false; + + async function processQueue() { + if (processing) return; + processing = true; + while (messageQueue.length > 0) { + const msg = messageQueue.shift(); + try { + const response = await handleMessage(msg); + if (response) { + process.stdout.write(JSON.stringify(response) + '\n'); + } + } catch (err) { + // Unexpected error in handleMessage — emit JSON-RPC internal error + const errorId = (typeof msg === 'object' && msg !== null) ? msg.id ?? null : null; + const errResp = { + jsonrpc: '2.0', + id: errorId, + error: { code: -32603, message: `Internal error: ${err.message}` }, + }; + process.stdout.write(JSON.stringify(errResp) + '\n'); + } + } + processing = false; + } process.stdin.setEncoding('utf8'); - process.stdin.on('data', async (chunk) => { + process.stdin.on('data', (chunk) => { buffer += chunk; // MCP uses newline-delimited JSON @@ -331,20 +366,26 @@ function startStdioTransport() { continue; } - const response = await handleMessage(msg); - if (response) { - process.stdout.write(JSON.stringify(response) + '\n'); - } + messageQueue.push(msg); } + + processQueue(); }); process.stdin.on('end', () => { process.exit(0); }); - // Prevent unhandled errors from crashing + // Prevent unhandled errors from crashing — log and exit process.on('uncaughtException', (err) => { - process.stderr.write(`MCP server error: ${err.message}\n`); + process.stderr.write(`MCP server uncaught exception: ${err.message}\n`); + process.exit(1); + }); + + process.on('unhandledRejection', (reason) => { + const msg = reason instanceof Error ? reason.message : String(reason); + process.stderr.write(`MCP server unhandled rejection: ${msg}\n`); + process.exit(1); }); }