sphere-mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +48 -0
- package/dist/api.js +95 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +75 -0
- package/package.json +37 -0
- package/src/api.ts +119 -0
- package/src/index.ts +15 -0
- package/src/tools.ts +185 -0
- package/tsconfig.json +14 -0
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
declare class SphereAPIError extends Error {
|
|
2
|
+
statusCode: number;
|
|
3
|
+
errorCode: string;
|
|
4
|
+
constructor(statusCode: number, errorCode: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function getPrices(symbols?: string[]): Promise<{
|
|
7
|
+
prices: unknown[];
|
|
8
|
+
}>;
|
|
9
|
+
export declare function getMarkets(stock: string): Promise<{
|
|
10
|
+
underlying: string;
|
|
11
|
+
grid: unknown[];
|
|
12
|
+
}>;
|
|
13
|
+
export declare function getPositions(wallet: string): Promise<unknown>;
|
|
14
|
+
export declare function getConfig(): Promise<unknown>;
|
|
15
|
+
export declare function getVolatility(): Promise<unknown>;
|
|
16
|
+
export declare function getQueue(): Promise<unknown>;
|
|
17
|
+
export declare function getLeaderboard(): Promise<unknown>;
|
|
18
|
+
export declare function getCashoutQuote(positionId: string): Promise<unknown>;
|
|
19
|
+
export declare function getStackCashoutQuote(stackId: string): Promise<unknown>;
|
|
20
|
+
export declare function prepareBet(params: {
|
|
21
|
+
marketId: string;
|
|
22
|
+
stake: number;
|
|
23
|
+
wallet: string;
|
|
24
|
+
}): Promise<unknown>;
|
|
25
|
+
export declare function confirmBet(params: {
|
|
26
|
+
positionId: string;
|
|
27
|
+
signedTx: string;
|
|
28
|
+
}): Promise<unknown>;
|
|
29
|
+
export declare function prepareStack(params: {
|
|
30
|
+
legs: {
|
|
31
|
+
marketId: string;
|
|
32
|
+
}[];
|
|
33
|
+
stake: number;
|
|
34
|
+
wallet: string;
|
|
35
|
+
}): Promise<unknown>;
|
|
36
|
+
export declare function confirmStack(params: {
|
|
37
|
+
stackId: string;
|
|
38
|
+
signedTx: string;
|
|
39
|
+
}): Promise<unknown>;
|
|
40
|
+
export declare function executeCashout(params: {
|
|
41
|
+
positionId: string;
|
|
42
|
+
wallet: string;
|
|
43
|
+
}): Promise<unknown>;
|
|
44
|
+
export declare function executeStackCashout(params: {
|
|
45
|
+
stackId: string;
|
|
46
|
+
wallet: string;
|
|
47
|
+
}): Promise<unknown>;
|
|
48
|
+
export { SphereAPIError };
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const BASE_URL = process.env.SPHERE_API_URL || "https://stack-888c1.web.app/v1";
|
|
2
|
+
class SphereAPIError extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
errorCode;
|
|
5
|
+
constructor(statusCode, errorCode, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
this.errorCode = errorCode;
|
|
9
|
+
this.name = "SphereAPIError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function request(path, options) {
|
|
13
|
+
const url = `${BASE_URL}${path}`;
|
|
14
|
+
const res = await fetch(url, {
|
|
15
|
+
...options,
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
...options?.headers,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const body = await res.json();
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const msg = body?.error || body?.message || `API error ${res.status}`;
|
|
24
|
+
const code = body?.code || `HTTP_${res.status}`;
|
|
25
|
+
throw new SphereAPIError(res.status, code, msg);
|
|
26
|
+
}
|
|
27
|
+
return body;
|
|
28
|
+
}
|
|
29
|
+
// --- Read ---
|
|
30
|
+
export async function getPrices(symbols) {
|
|
31
|
+
const query = symbols?.length ? `?symbols=${symbols.join(",")}` : "";
|
|
32
|
+
return request(`/prices${query}`);
|
|
33
|
+
}
|
|
34
|
+
export async function getMarkets(stock) {
|
|
35
|
+
return request(`/sphere/markets/${stock}`);
|
|
36
|
+
}
|
|
37
|
+
export async function getPositions(wallet) {
|
|
38
|
+
return request(`/sphere/positions/${wallet}`);
|
|
39
|
+
}
|
|
40
|
+
export async function getConfig() {
|
|
41
|
+
return request("/sphere/config");
|
|
42
|
+
}
|
|
43
|
+
export async function getVolatility() {
|
|
44
|
+
return request("/sphere/volatility");
|
|
45
|
+
}
|
|
46
|
+
export async function getQueue() {
|
|
47
|
+
return request("/sphere/queue");
|
|
48
|
+
}
|
|
49
|
+
export async function getLeaderboard() {
|
|
50
|
+
return request("/sphere/leaderboard");
|
|
51
|
+
}
|
|
52
|
+
export async function getCashoutQuote(positionId) {
|
|
53
|
+
return request(`/sphere/cashout/quote/${positionId}`);
|
|
54
|
+
}
|
|
55
|
+
export async function getStackCashoutQuote(stackId) {
|
|
56
|
+
return request(`/sphere/stack/cashout/quote/${stackId}`);
|
|
57
|
+
}
|
|
58
|
+
// --- Write ---
|
|
59
|
+
export async function prepareBet(params) {
|
|
60
|
+
return request("/sphere/bet", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: JSON.stringify({ ...params, source: "mcp" }),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export async function confirmBet(params) {
|
|
66
|
+
return request("/sphere/bet/confirm", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: JSON.stringify(params),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export async function prepareStack(params) {
|
|
72
|
+
return request("/sphere/stack/bet", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: JSON.stringify({ ...params, source: "mcp" }),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export async function confirmStack(params) {
|
|
78
|
+
return request("/sphere/stack/bet/confirm", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
body: JSON.stringify(params),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function executeCashout(params) {
|
|
84
|
+
return request("/sphere/cashout/execute", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
body: JSON.stringify(params),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function executeStackCashout(params) {
|
|
90
|
+
return request("/sphere/stack/cashout/execute", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: JSON.stringify(params),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export { SphereAPIError };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { registerTools } from "./tools.js";
|
|
5
|
+
const server = new McpServer({
|
|
6
|
+
name: "Sphere",
|
|
7
|
+
version: "0.1.0",
|
|
8
|
+
});
|
|
9
|
+
registerTools(server);
|
|
10
|
+
const transport = new StdioServerTransport();
|
|
11
|
+
await server.connect(transport);
|
package/dist/tools.d.ts
ADDED
package/dist/tools.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as api from "./api.js";
|
|
3
|
+
import { SphereAPIError } from "./api.js";
|
|
4
|
+
function resolveWallet(input) {
|
|
5
|
+
const wallet = input || process.env.SPHERE_WALLET_ADDRESS;
|
|
6
|
+
if (!wallet)
|
|
7
|
+
throw new Error("Wallet address required. Pass it as a parameter or set SPHERE_WALLET_ADDRESS env var.");
|
|
8
|
+
return wallet;
|
|
9
|
+
}
|
|
10
|
+
function ok(data) {
|
|
11
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
12
|
+
}
|
|
13
|
+
function fail(code, message, details) {
|
|
14
|
+
return {
|
|
15
|
+
content: [{
|
|
16
|
+
type: "text",
|
|
17
|
+
text: JSON.stringify({ ok: false, errorCode: code, message, ...(details ? { details } : {}) }),
|
|
18
|
+
}],
|
|
19
|
+
isError: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function wrap(fn) {
|
|
23
|
+
try {
|
|
24
|
+
return ok(await fn());
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (e instanceof SphereAPIError) {
|
|
28
|
+
return fail(e.errorCode, e.message, { statusCode: e.statusCode });
|
|
29
|
+
}
|
|
30
|
+
return fail("UNKNOWN_ERROR", e instanceof Error ? e.message : "Unknown error");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function registerTools(server) {
|
|
34
|
+
// ──────────────────────────────────────────────
|
|
35
|
+
// Read tools
|
|
36
|
+
// ──────────────────────────────────────────────
|
|
37
|
+
server.tool("get_prices", "Get current prices for all 14 supported stocks (TSLA, NVDA, AAPL, SPY, etc). Returns price, 24h change, and volume.", { symbols: z.array(z.string()).optional().describe("Optional filter: stock symbols (e.g. ['TSLAx', 'NVDAx'])") }, { readOnlyHint: true }, async ({ symbols }) => wrap(() => api.getPrices(symbols)));
|
|
38
|
+
server.tool("get_markets", "Get the betting grid for a stock showing all available markets with multipliers. Each cell is a direction (UP/DOWN) x move size (1-20%) x tenor (1H/1D/7D/30D) combination. Example: get_markets({ stock: 'TSLAx' })", { stock: z.string().describe("Stock symbol (e.g. TSLAx, NVDAx, SPYx)") }, { readOnlyHint: true }, async ({ stock }) => wrap(() => api.getMarkets(stock)));
|
|
39
|
+
server.tool("get_positions", "Get all positions for a wallet: pending, active, settled singles and stacks, plus summary stats.", { wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)") }, { readOnlyHint: true }, async ({ wallet }) => wrap(() => api.getPositions(resolveWallet(wallet))));
|
|
40
|
+
server.tool("get_protocol_config", "Get Sphere protocol parameters: fees, bet limits, valid tenors, move sizes, 1H trading hour restrictions, and cash-out rules.", {}, { readOnlyHint: true }, async () => wrap(() => api.getConfig()));
|
|
41
|
+
server.tool("get_volatility", "Get realized annualized volatility for each stock. Used by the pricing model to calculate multipliers.", {}, { readOnlyHint: true }, async () => wrap(() => api.getVolatility()));
|
|
42
|
+
server.tool("get_settlement_queue", "Get the public settlement queue: upcoming bets expiring soon and recently settled positions.", {}, { readOnlyHint: true }, async () => wrap(() => api.getQueue()));
|
|
43
|
+
server.tool("get_leaderboard", "Get the top 10 all-time biggest wins on Sphere by payout amount.", {}, { readOnlyHint: true }, async () => wrap(() => api.getLeaderboard()));
|
|
44
|
+
server.tool("get_cashout_quote", "Get a cash-out quote for an active single bet. Returns current value, live probability, and eligibility.", { positionId: z.string().describe("Position ID to quote") }, { readOnlyHint: true }, async ({ positionId }) => wrap(() => api.getCashoutQuote(positionId)));
|
|
45
|
+
server.tool("get_stack_cashout_quote", "Get a cash-out quote for an active stack/parlay. Returns combined probability, per-leg probs, and value.", { stackId: z.string().describe("Stack ID to quote") }, { readOnlyHint: true }, async ({ stackId }) => wrap(() => api.getStackCashoutQuote(stackId)));
|
|
46
|
+
// ──────────────────────────────────────────────
|
|
47
|
+
// Write tools
|
|
48
|
+
// ──────────────────────────────────────────────
|
|
49
|
+
server.tool("prepare_bet", "Prepare a single bet. Returns an unsigned Solana transaction that must be signed and confirmed via confirm_bet. Flow: get_markets → prepare_bet → user signs tx → confirm_bet.", {
|
|
50
|
+
marketId: z.string().describe("Market ID from get_markets grid (e.g. 'TSLAx-UP-500-7D')"),
|
|
51
|
+
stake: z.number().describe("USDC amount to bet (min $0.10, max $500, max $100 for 1H)"),
|
|
52
|
+
wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)"),
|
|
53
|
+
}, { destructiveHint: true }, async ({ marketId, stake, wallet }) => wrap(() => api.prepareBet({ marketId, stake, wallet: resolveWallet(wallet) })));
|
|
54
|
+
server.tool("confirm_bet", "Confirm a prepared bet after the user has signed the transaction. Completes the bet placement.", {
|
|
55
|
+
positionId: z.string().describe("Position ID from prepare_bet response"),
|
|
56
|
+
signedTx: z.string().describe("Base64-encoded signed Solana transaction"),
|
|
57
|
+
}, { destructiveHint: true }, async ({ positionId, signedTx }) => wrap(() => api.confirmBet({ positionId, signedTx })));
|
|
58
|
+
server.tool("prepare_stack", "Prepare a parlay/stack bet with 2-5 legs. Each leg must be a different stock. Returns an unsigned Solana transaction. Flow: get_markets (for each stock) → prepare_stack → user signs → confirm_stack.", {
|
|
59
|
+
legs: z.array(z.object({ marketId: z.string() })).min(2).max(5).describe("2-5 legs, each with a marketId from get_markets"),
|
|
60
|
+
stake: z.number().describe("USDC amount to bet"),
|
|
61
|
+
wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)"),
|
|
62
|
+
}, { destructiveHint: true }, async ({ legs, stake, wallet }) => wrap(() => api.prepareStack({ legs, stake, wallet: resolveWallet(wallet) })));
|
|
63
|
+
server.tool("confirm_stack", "Confirm a prepared stack/parlay bet after the user has signed the transaction.", {
|
|
64
|
+
stackId: z.string().describe("Stack ID from prepare_stack response"),
|
|
65
|
+
signedTx: z.string().describe("Base64-encoded signed Solana transaction"),
|
|
66
|
+
}, { destructiveHint: true }, async ({ stackId, signedTx }) => wrap(() => api.confirmStack({ stackId, signedTx })));
|
|
67
|
+
server.tool("execute_cashout", "Cash out an active single bet at the current live probability price. The protocol handles the on-chain transfer.", {
|
|
68
|
+
positionId: z.string().describe("Position ID to cash out"),
|
|
69
|
+
wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)"),
|
|
70
|
+
}, { destructiveHint: true }, async ({ positionId, wallet }) => wrap(() => api.executeCashout({ positionId, wallet: resolveWallet(wallet) })));
|
|
71
|
+
server.tool("execute_stack_cashout", "Cash out an active stack/parlay at the current combined probability price. The protocol handles the on-chain transfer.", {
|
|
72
|
+
stackId: z.string().describe("Stack ID to cash out"),
|
|
73
|
+
wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)"),
|
|
74
|
+
}, { destructiveHint: true }, async ({ stackId, wallet }) => wrap(() => api.executeStackCashout({ stackId, wallet: resolveWallet(wallet) })));
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sphere-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Sphere — binary betting on stocks via Solana",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sphere-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"solana",
|
|
22
|
+
"defi",
|
|
23
|
+
"trading",
|
|
24
|
+
"betting",
|
|
25
|
+
"finance",
|
|
26
|
+
"crypto",
|
|
27
|
+
"prediction-markets",
|
|
28
|
+
"stocks"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"typescript": "^5.5.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const BASE_URL = process.env.SPHERE_API_URL || "https://stack-888c1.web.app/v1";
|
|
2
|
+
|
|
3
|
+
class SphereAPIError extends Error {
|
|
4
|
+
constructor(
|
|
5
|
+
public statusCode: number,
|
|
6
|
+
public errorCode: string,
|
|
7
|
+
message: string,
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "SphereAPIError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
|
15
|
+
const url = `${BASE_URL}${path}`;
|
|
16
|
+
|
|
17
|
+
const res = await fetch(url, {
|
|
18
|
+
...options,
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
...options?.headers,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const body = await res.json();
|
|
26
|
+
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const msg = body?.error || body?.message || `API error ${res.status}`;
|
|
29
|
+
const code = body?.code || `HTTP_${res.status}`;
|
|
30
|
+
throw new SphereAPIError(res.status, code, msg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return body as T;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Read ---
|
|
37
|
+
|
|
38
|
+
export async function getPrices(symbols?: string[]) {
|
|
39
|
+
const query = symbols?.length ? `?symbols=${symbols.join(",")}` : "";
|
|
40
|
+
return request<{ prices: unknown[] }>(`/prices${query}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function getMarkets(stock: string) {
|
|
44
|
+
return request<{ underlying: string; grid: unknown[] }>(`/sphere/markets/${stock}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getPositions(wallet: string) {
|
|
48
|
+
return request<unknown>(`/sphere/positions/${wallet}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getConfig() {
|
|
52
|
+
return request<unknown>("/sphere/config");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getVolatility() {
|
|
56
|
+
return request<unknown>("/sphere/volatility");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getQueue() {
|
|
60
|
+
return request<unknown>("/sphere/queue");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getLeaderboard() {
|
|
64
|
+
return request<unknown>("/sphere/leaderboard");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getCashoutQuote(positionId: string) {
|
|
68
|
+
return request<unknown>(`/sphere/cashout/quote/${positionId}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getStackCashoutQuote(stackId: string) {
|
|
72
|
+
return request<unknown>(`/sphere/stack/cashout/quote/${stackId}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Write ---
|
|
76
|
+
|
|
77
|
+
export async function prepareBet(params: { marketId: string; stake: number; wallet: string }) {
|
|
78
|
+
return request<unknown>("/sphere/bet", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
body: JSON.stringify({ ...params, source: "mcp" }),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function confirmBet(params: { positionId: string; signedTx: string }) {
|
|
85
|
+
return request<unknown>("/sphere/bet/confirm", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
body: JSON.stringify(params),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function prepareStack(params: { legs: { marketId: string }[]; stake: number; wallet: string }) {
|
|
92
|
+
return request<unknown>("/sphere/stack/bet", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: JSON.stringify({ ...params, source: "mcp" }),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function confirmStack(params: { stackId: string; signedTx: string }) {
|
|
99
|
+
return request<unknown>("/sphere/stack/bet/confirm", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
body: JSON.stringify(params),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function executeCashout(params: { positionId: string; wallet: string }) {
|
|
106
|
+
return request<unknown>("/sphere/cashout/execute", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
body: JSON.stringify(params),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function executeStackCashout(params: { stackId: string; wallet: string }) {
|
|
113
|
+
return request<unknown>("/sphere/stack/cashout/execute", {
|
|
114
|
+
method: "POST",
|
|
115
|
+
body: JSON.stringify(params),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { SphereAPIError };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { registerTools } from "./tools.js";
|
|
6
|
+
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "Sphere",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
registerTools(server);
|
|
13
|
+
|
|
14
|
+
const transport = new StdioServerTransport();
|
|
15
|
+
await server.connect(transport);
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import * as api from "./api.js";
|
|
4
|
+
import { SphereAPIError } from "./api.js";
|
|
5
|
+
|
|
6
|
+
function resolveWallet(input?: string): string {
|
|
7
|
+
const wallet = input || process.env.SPHERE_WALLET_ADDRESS;
|
|
8
|
+
if (!wallet) throw new Error("Wallet address required. Pass it as a parameter or set SPHERE_WALLET_ADDRESS env var.");
|
|
9
|
+
return wallet;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ok(data: unknown) {
|
|
13
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fail(code: string, message: string, details?: unknown) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{
|
|
19
|
+
type: "text" as const,
|
|
20
|
+
text: JSON.stringify({ ok: false, errorCode: code, message, ...(details ? { details } : {}) }),
|
|
21
|
+
}],
|
|
22
|
+
isError: true as const,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function wrap<T>(fn: () => Promise<T>) {
|
|
27
|
+
try {
|
|
28
|
+
return ok(await fn());
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (e instanceof SphereAPIError) {
|
|
31
|
+
return fail(e.errorCode, e.message, { statusCode: e.statusCode });
|
|
32
|
+
}
|
|
33
|
+
return fail("UNKNOWN_ERROR", e instanceof Error ? e.message : "Unknown error");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function registerTools(server: McpServer) {
|
|
38
|
+
// ──────────────────────────────────────────────
|
|
39
|
+
// Read tools
|
|
40
|
+
// ──────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
server.tool(
|
|
43
|
+
"get_prices",
|
|
44
|
+
"Get current prices for all 14 supported stocks (TSLA, NVDA, AAPL, SPY, etc). Returns price, 24h change, and volume.",
|
|
45
|
+
{ symbols: z.array(z.string()).optional().describe("Optional filter: stock symbols (e.g. ['TSLAx', 'NVDAx'])") },
|
|
46
|
+
{ readOnlyHint: true },
|
|
47
|
+
async ({ symbols }) => wrap(() => api.getPrices(symbols)),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
server.tool(
|
|
51
|
+
"get_markets",
|
|
52
|
+
"Get the betting grid for a stock showing all available markets with multipliers. Each cell is a direction (UP/DOWN) x move size (1-20%) x tenor (1H/1D/7D/30D) combination. Example: get_markets({ stock: 'TSLAx' })",
|
|
53
|
+
{ stock: z.string().describe("Stock symbol (e.g. TSLAx, NVDAx, SPYx)") },
|
|
54
|
+
{ readOnlyHint: true },
|
|
55
|
+
async ({ stock }) => wrap(() => api.getMarkets(stock)),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
server.tool(
|
|
59
|
+
"get_positions",
|
|
60
|
+
"Get all positions for a wallet: pending, active, settled singles and stacks, plus summary stats.",
|
|
61
|
+
{ wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)") },
|
|
62
|
+
{ readOnlyHint: true },
|
|
63
|
+
async ({ wallet }) => wrap(() => api.getPositions(resolveWallet(wallet))),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
server.tool(
|
|
67
|
+
"get_protocol_config",
|
|
68
|
+
"Get Sphere protocol parameters: fees, bet limits, valid tenors, move sizes, 1H trading hour restrictions, and cash-out rules.",
|
|
69
|
+
{},
|
|
70
|
+
{ readOnlyHint: true },
|
|
71
|
+
async () => wrap(() => api.getConfig()),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.tool(
|
|
75
|
+
"get_volatility",
|
|
76
|
+
"Get realized annualized volatility for each stock. Used by the pricing model to calculate multipliers.",
|
|
77
|
+
{},
|
|
78
|
+
{ readOnlyHint: true },
|
|
79
|
+
async () => wrap(() => api.getVolatility()),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
server.tool(
|
|
83
|
+
"get_settlement_queue",
|
|
84
|
+
"Get the public settlement queue: upcoming bets expiring soon and recently settled positions.",
|
|
85
|
+
{},
|
|
86
|
+
{ readOnlyHint: true },
|
|
87
|
+
async () => wrap(() => api.getQueue()),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
server.tool(
|
|
91
|
+
"get_leaderboard",
|
|
92
|
+
"Get the top 10 all-time biggest wins on Sphere by payout amount.",
|
|
93
|
+
{},
|
|
94
|
+
{ readOnlyHint: true },
|
|
95
|
+
async () => wrap(() => api.getLeaderboard()),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
server.tool(
|
|
99
|
+
"get_cashout_quote",
|
|
100
|
+
"Get a cash-out quote for an active single bet. Returns current value, live probability, and eligibility.",
|
|
101
|
+
{ positionId: z.string().describe("Position ID to quote") },
|
|
102
|
+
{ readOnlyHint: true },
|
|
103
|
+
async ({ positionId }) => wrap(() => api.getCashoutQuote(positionId)),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
server.tool(
|
|
107
|
+
"get_stack_cashout_quote",
|
|
108
|
+
"Get a cash-out quote for an active stack/parlay. Returns combined probability, per-leg probs, and value.",
|
|
109
|
+
{ stackId: z.string().describe("Stack ID to quote") },
|
|
110
|
+
{ readOnlyHint: true },
|
|
111
|
+
async ({ stackId }) => wrap(() => api.getStackCashoutQuote(stackId)),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// ──────────────────────────────────────────────
|
|
115
|
+
// Write tools
|
|
116
|
+
// ──────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
server.tool(
|
|
119
|
+
"prepare_bet",
|
|
120
|
+
"Prepare a single bet. Returns an unsigned Solana transaction that must be signed and confirmed via confirm_bet. Flow: get_markets → prepare_bet → user signs tx → confirm_bet.",
|
|
121
|
+
{
|
|
122
|
+
marketId: z.string().describe("Market ID from get_markets grid (e.g. 'TSLAx-UP-500-7D')"),
|
|
123
|
+
stake: z.number().describe("USDC amount to bet (min $0.10, max $500, max $100 for 1H)"),
|
|
124
|
+
wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)"),
|
|
125
|
+
},
|
|
126
|
+
{ destructiveHint: true },
|
|
127
|
+
async ({ marketId, stake, wallet }) => wrap(() => api.prepareBet({ marketId, stake, wallet: resolveWallet(wallet) })),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
server.tool(
|
|
131
|
+
"confirm_bet",
|
|
132
|
+
"Confirm a prepared bet after the user has signed the transaction. Completes the bet placement.",
|
|
133
|
+
{
|
|
134
|
+
positionId: z.string().describe("Position ID from prepare_bet response"),
|
|
135
|
+
signedTx: z.string().describe("Base64-encoded signed Solana transaction"),
|
|
136
|
+
},
|
|
137
|
+
{ destructiveHint: true },
|
|
138
|
+
async ({ positionId, signedTx }) => wrap(() => api.confirmBet({ positionId, signedTx })),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
server.tool(
|
|
142
|
+
"prepare_stack",
|
|
143
|
+
"Prepare a parlay/stack bet with 2-5 legs. Each leg must be a different stock. Returns an unsigned Solana transaction. Flow: get_markets (for each stock) → prepare_stack → user signs → confirm_stack.",
|
|
144
|
+
{
|
|
145
|
+
legs: z.array(z.object({ marketId: z.string() })).min(2).max(5).describe("2-5 legs, each with a marketId from get_markets"),
|
|
146
|
+
stake: z.number().describe("USDC amount to bet"),
|
|
147
|
+
wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)"),
|
|
148
|
+
},
|
|
149
|
+
{ destructiveHint: true },
|
|
150
|
+
async ({ legs, stake, wallet }) => wrap(() => api.prepareStack({ legs, stake, wallet: resolveWallet(wallet) })),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
server.tool(
|
|
154
|
+
"confirm_stack",
|
|
155
|
+
"Confirm a prepared stack/parlay bet after the user has signed the transaction.",
|
|
156
|
+
{
|
|
157
|
+
stackId: z.string().describe("Stack ID from prepare_stack response"),
|
|
158
|
+
signedTx: z.string().describe("Base64-encoded signed Solana transaction"),
|
|
159
|
+
},
|
|
160
|
+
{ destructiveHint: true },
|
|
161
|
+
async ({ stackId, signedTx }) => wrap(() => api.confirmStack({ stackId, signedTx })),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
server.tool(
|
|
165
|
+
"execute_cashout",
|
|
166
|
+
"Cash out an active single bet at the current live probability price. The protocol handles the on-chain transfer.",
|
|
167
|
+
{
|
|
168
|
+
positionId: z.string().describe("Position ID to cash out"),
|
|
169
|
+
wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)"),
|
|
170
|
+
},
|
|
171
|
+
{ destructiveHint: true },
|
|
172
|
+
async ({ positionId, wallet }) => wrap(() => api.executeCashout({ positionId, wallet: resolveWallet(wallet) })),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
server.tool(
|
|
176
|
+
"execute_stack_cashout",
|
|
177
|
+
"Cash out an active stack/parlay at the current combined probability price. The protocol handles the on-chain transfer.",
|
|
178
|
+
{
|
|
179
|
+
stackId: z.string().describe("Stack ID to cash out"),
|
|
180
|
+
wallet: z.string().optional().describe("Solana wallet address (optional if SPHERE_WALLET_ADDRESS is set)"),
|
|
181
|
+
},
|
|
182
|
+
{ destructiveHint: true },
|
|
183
|
+
async ({ stackId, wallet }) => wrap(() => api.executeStackCashout({ stackId, wallet: resolveWallet(wallet) })),
|
|
184
|
+
);
|
|
185
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|