@orbs-network/spot-mcp 2.4.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/package.json +31 -0
- package/stdio.mjs +240 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orbs-network/spot-mcp",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"mcpName": "io.github.orbs-network/spot",
|
|
5
|
+
"description": "MCP adapter for Spot Advanced Swap Orders.",
|
|
6
|
+
"homepage": "https://orbs-network.github.io/spot/",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/orbs-network/spot/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/orbs-network/spot.git",
|
|
13
|
+
"directory": "mcp"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "danielz@orbs.com",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"spot-mcp": "./stdio.mjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"stdio.mjs"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
28
|
+
"zod": "4.3.6",
|
|
29
|
+
"@orbs-network/spot-skill": "2.4.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/stdio.mjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const packageDir = __dirname;
|
|
15
|
+
const packageJsonPath = path.join(packageDir, "package.json");
|
|
16
|
+
const skillDir = resolveSkillDir();
|
|
17
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
18
|
+
const cliPath = path.join(skillDir, "scripts", "order.js");
|
|
19
|
+
|
|
20
|
+
const [pkg, skillMd] = await Promise.all([
|
|
21
|
+
readJsonFile(packageJsonPath),
|
|
22
|
+
readFile(skillMdPath, "utf8"),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const mcpName = requiredString(pkg.mcpName, "package.json mcpName");
|
|
26
|
+
const version = requiredString(pkg.version, "package.json version");
|
|
27
|
+
const skillFrontmatter = parseFrontmatter(skillMd);
|
|
28
|
+
const skillName = requiredString(skillFrontmatter.name, "skill frontmatter name");
|
|
29
|
+
const skillTitle = parseSkillTitle(skillMd);
|
|
30
|
+
|
|
31
|
+
const server = new McpServer({
|
|
32
|
+
name: mcpName,
|
|
33
|
+
version,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
server.registerResource(
|
|
37
|
+
"spot-skill",
|
|
38
|
+
"spot://skill",
|
|
39
|
+
{
|
|
40
|
+
title: `${skillTitle} skill`,
|
|
41
|
+
description: `Canonical ${skillName} SKILL.md with inline metadata for ${mcpName}`,
|
|
42
|
+
mimeType: "text/markdown",
|
|
43
|
+
},
|
|
44
|
+
async (uri) => ({
|
|
45
|
+
contents: [
|
|
46
|
+
{
|
|
47
|
+
uri: uri.href,
|
|
48
|
+
text: await readFile(skillMdPath, "utf8"),
|
|
49
|
+
mimeType: "text/markdown",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
server.registerTool(
|
|
56
|
+
"prepare_order",
|
|
57
|
+
{
|
|
58
|
+
title: "Prepare order",
|
|
59
|
+
description: "Prepare approval calldata, typed data, submit payload, and query URL.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
params: z.record(z.string(), z.unknown()),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
async ({ params }) => runTool(["prepare", "--params", "-"], params),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
server.registerTool(
|
|
68
|
+
"submit_order",
|
|
69
|
+
{
|
|
70
|
+
title: "Submit order",
|
|
71
|
+
description: "Submit a prepared order with a signature.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
prepared: z.unknown(),
|
|
74
|
+
signature: z.union([
|
|
75
|
+
z.string(),
|
|
76
|
+
z.object({
|
|
77
|
+
r: z.string(),
|
|
78
|
+
s: z.string(),
|
|
79
|
+
v: z.union([z.string(), z.number()]),
|
|
80
|
+
}),
|
|
81
|
+
]),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
async ({ prepared, signature }) => {
|
|
85
|
+
const signatureArgs =
|
|
86
|
+
typeof signature === "string"
|
|
87
|
+
? ["--signature", signature]
|
|
88
|
+
: ["--signature", JSON.stringify(signature)];
|
|
89
|
+
|
|
90
|
+
return runTool(["submit", "--prepared", "-", ...signatureArgs], prepared);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
server.registerTool(
|
|
95
|
+
"query_orders",
|
|
96
|
+
{
|
|
97
|
+
title: "Query orders",
|
|
98
|
+
description: "Query orders by swapper and/or order hash.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
swapper: z.string().optional(),
|
|
101
|
+
hash: z.string().optional(),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
async ({ swapper, hash }) => {
|
|
105
|
+
if (!swapper && !hash) {
|
|
106
|
+
return toolError("query_orders requires swapper or hash");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const args = ["query"];
|
|
110
|
+
if (swapper) {
|
|
111
|
+
args.push("--swapper", swapper);
|
|
112
|
+
}
|
|
113
|
+
if (hash) {
|
|
114
|
+
args.push("--hash", hash);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return runTool(args);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
await server.connect(new StdioServerTransport());
|
|
122
|
+
|
|
123
|
+
async function readJsonFile(filePath) {
|
|
124
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function requiredString(value, label) {
|
|
128
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
129
|
+
throw new Error(`${label} is required`);
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseFrontmatter(markdown) {
|
|
135
|
+
const match = markdown.match(/^---\n([\s\S]*?)\n---\n/);
|
|
136
|
+
if (!match) {
|
|
137
|
+
throw new Error("skill frontmatter is required");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return Object.fromEntries(
|
|
141
|
+
match[1]
|
|
142
|
+
.split("\n")
|
|
143
|
+
.map((line) => line.trim())
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.map((line) => {
|
|
146
|
+
const splitIndex = line.indexOf(":");
|
|
147
|
+
if (splitIndex === -1) {
|
|
148
|
+
throw new Error(`invalid frontmatter line: ${line}`);
|
|
149
|
+
}
|
|
150
|
+
return [line.slice(0, splitIndex).trim(), line.slice(splitIndex + 1).trim()];
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseSkillTitle(markdown) {
|
|
156
|
+
const match = markdown.match(/^#\s+(.+)$/m);
|
|
157
|
+
if (!match) {
|
|
158
|
+
throw new Error("skill title heading is required");
|
|
159
|
+
}
|
|
160
|
+
return requiredString(match[1], "skill title");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function runCli(args, stdinJson) {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
166
|
+
cwd: process.cwd(),
|
|
167
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
let stdout = "";
|
|
171
|
+
let stderr = "";
|
|
172
|
+
|
|
173
|
+
child.stdout.on("data", (chunk) => {
|
|
174
|
+
stdout += String(chunk);
|
|
175
|
+
});
|
|
176
|
+
child.stderr.on("data", (chunk) => {
|
|
177
|
+
stderr += String(chunk);
|
|
178
|
+
});
|
|
179
|
+
child.on("error", reject);
|
|
180
|
+
|
|
181
|
+
child.on("close", (code) => {
|
|
182
|
+
const out = stdout.trim();
|
|
183
|
+
const err = stderr.trim();
|
|
184
|
+
|
|
185
|
+
if (code !== 0) {
|
|
186
|
+
reject(new Error(err || out || `order.js exited with code ${code}`));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
resolve(out ? JSON.parse(out) : {});
|
|
192
|
+
} catch {
|
|
193
|
+
resolve({
|
|
194
|
+
raw: out,
|
|
195
|
+
stderr: err,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const payload = stdinJson == null ? "" : `${JSON.stringify(stdinJson)}\n`;
|
|
201
|
+
child.stdin.end(payload);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function runTool(args, stdinJson) {
|
|
206
|
+
try {
|
|
207
|
+
const output = await runCli(args, stdinJson);
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: JSON.stringify(output, null, 2),
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
structuredContent: output,
|
|
216
|
+
};
|
|
217
|
+
} catch (error) {
|
|
218
|
+
return toolError(String(error?.message ?? error));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function toolError(message) {
|
|
223
|
+
return {
|
|
224
|
+
isError: true,
|
|
225
|
+
content: [
|
|
226
|
+
{
|
|
227
|
+
type: "text",
|
|
228
|
+
text: message,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function resolveSkillDir() {
|
|
235
|
+
try {
|
|
236
|
+
return path.dirname(require.resolve("@orbs-network/spot-skill/package.json"));
|
|
237
|
+
} catch {
|
|
238
|
+
return path.resolve(packageDir, "..", "skill");
|
|
239
|
+
}
|
|
240
|
+
}
|