context-markets-mcp 0.1.2 → 0.2.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.
- package/README.md +83 -16
- package/dist/index.js +21 -7
- package/dist/lib/client.d.ts +2 -0
- package/dist/lib/client.js +39 -21
- package/dist/lib/config.d.ts +6 -0
- package/dist/lib/config.js +52 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/tools/account.js +180 -5
- package/dist/tools/orders.js +191 -17
- package/dist/tools/portfolio.js +2 -2
- package/dist/tools/questions.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -28,34 +28,101 @@ Add to your `claude_desktop_config.json`:
|
|
|
28
28
|
"mcpServers": {
|
|
29
29
|
"context-markets": {
|
|
30
30
|
"command": "npx",
|
|
31
|
-
"args": ["context-markets-mcp"]
|
|
32
|
-
"env": {
|
|
33
|
-
"CONTEXT_API_KEY": "your-api-key",
|
|
34
|
-
"CONTEXT_PRIVATE_KEY": "your-wallet-private-key"
|
|
35
|
-
}
|
|
31
|
+
"args": ["context-markets-mcp"]
|
|
36
32
|
}
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
35
|
```
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
No environment variables needed — the server walks you through setup on first use.
|
|
38
|
+
|
|
39
|
+
## Getting Started
|
|
42
40
|
|
|
43
|
-
|
|
41
|
+
The first time you (or your agent) use a trading tool, the server will guide you through onboarding:
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
1. **Wallet** — `context_generate_wallet` creates a new wallet or imports an existing private key
|
|
44
|
+
2. **Save credentials** — persists to `~/.config/context/config.env` (chmod 600, shared with the [CLI](https://github.com/contextwtf/context-cli))
|
|
45
|
+
3. **API key** — pass your Context API key (get one at [context.markets](https://context.markets)) via the `apiKey` param
|
|
46
|
+
4. **Approve contracts** — `context_account_setup` approves on-chain (requires ETH on Base for gas)
|
|
47
|
+
5. **Deposit USDC** — `context_deposit` deposits USDC into the exchange
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
If anything fails (no ETH, rate limit, etc.), you can re-run the same tool — it detects your existing config and picks up where you left off.
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
### Manual setup
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
If you prefer to configure manually:
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
```bash
|
|
56
|
+
# Option 1: Environment variables
|
|
57
|
+
export CONTEXT_API_KEY="your-api-key"
|
|
58
|
+
export CONTEXT_PRIVATE_KEY="0x..."
|
|
59
|
+
|
|
60
|
+
# Option 2: Config file (created by context_generate_wallet or `context setup` in the CLI)
|
|
61
|
+
# ~/.config/context/config.env
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Credentials are loaded in order: env vars > config file.
|
|
65
|
+
|
|
66
|
+
Need an API key? Visit [context.markets](https://context.markets).
|
|
67
|
+
|
|
68
|
+
## Available Tools
|
|
57
69
|
|
|
58
|
-
Read-only
|
|
70
|
+
### Read-only (no wallet needed)
|
|
71
|
+
|
|
72
|
+
| Tool | Description | Key Params |
|
|
73
|
+
|------|-------------|------------|
|
|
74
|
+
| `context_list_markets` | List and search prediction markets on Context Markets | `query`, `status`, `category`, `sortBy`, `limit` |
|
|
75
|
+
| `context_get_market` | Get detailed information about a specific prediction market | `marketId` |
|
|
76
|
+
| `context_get_quotes` | Get current bid/ask/last prices for a market's YES and NO outcomes | `marketId` |
|
|
77
|
+
| `context_get_orderbook` | Get the orderbook (bid/ask ladder) for a market | `marketId`, `depth` |
|
|
78
|
+
| `context_simulate_trade` | Simulate a trade to estimate fill price, cost, and slippage | `marketId`, `side`, `amount` |
|
|
79
|
+
| `context_price_history` | Get historical price data for a market over a specified timeframe | `marketId`, `timeframe` |
|
|
80
|
+
| `context_get_oracle` | Get the AI oracle's resolution analysis for a market | `marketId` |
|
|
81
|
+
| `context_global_activity` | Get recent trading activity across all markets | -- |
|
|
82
|
+
|
|
83
|
+
### Account setup and funding
|
|
84
|
+
|
|
85
|
+
| Tool | Description | Key Params |
|
|
86
|
+
|------|-------------|------------|
|
|
87
|
+
| `context_generate_wallet` | Generate a new wallet or import an existing key | `privateKey`, `apiKey`, `overwrite` |
|
|
88
|
+
| `context_wallet_status` | Get address, balances, and approval status | -- |
|
|
89
|
+
| `context_account_setup` | Approve USDC spending and operator permissions | -- |
|
|
90
|
+
| `context_deposit` | Deposit USDC into the exchange | `amount` |
|
|
91
|
+
| `context_withdraw` | Withdraw USDC from the exchange back to your wallet | `amount` |
|
|
92
|
+
| `context_mint_test_usdc` | Mint test USDC on Base Sepolia | `amount` |
|
|
93
|
+
|
|
94
|
+
### Trading and orders
|
|
95
|
+
|
|
96
|
+
| Tool | Description | Key Params |
|
|
97
|
+
|------|-------------|------------|
|
|
98
|
+
| `context_place_order` | Place a buy or sell order on a prediction market | `marketId`, `outcome`, `side`, `size`, `price` |
|
|
99
|
+
| `context_cancel_order` | Cancel an open order | `nonce` |
|
|
100
|
+
| `context_cancel_replace_order` | Atomically cancel an existing order and place a new one | `cancelNonce`, `marketId`, `outcome`, `side`, `priceCents`, `size` |
|
|
101
|
+
| `context_my_orders` | List your open orders | `marketId` |
|
|
102
|
+
| `context_bulk_create_orders` | Create multiple orders in a single atomic batch | `orders` |
|
|
103
|
+
| `context_bulk_cancel_orders` | Cancel multiple open orders in a single batch | `nonces` |
|
|
104
|
+
| `context_bulk_orders` | Atomically create and cancel orders in a single batch | `creates`, `cancelNonces` |
|
|
105
|
+
|
|
106
|
+
### Portfolio
|
|
107
|
+
|
|
108
|
+
| Tool | Description | Key Params |
|
|
109
|
+
|------|-------------|------------|
|
|
110
|
+
| `context_get_portfolio` | Get positions with P&L | `kind` |
|
|
111
|
+
| `context_get_balance` | Get USDC balance and token holdings | -- |
|
|
112
|
+
|
|
113
|
+
### Market creation
|
|
114
|
+
|
|
115
|
+
| Tool | Description | Key Params |
|
|
116
|
+
|------|-------------|------------|
|
|
117
|
+
| `context_create_market` | Create a market from a question | `question` |
|
|
118
|
+
| `context_agent_submit_market` | Submit a fully formed market draft, wait for oracle approval, and create it on-chain | `formattedQuestion`, `shortQuestion`, `marketType`, `evidenceMode`, `resolutionCriteria`, `endTime` |
|
|
119
|
+
|
|
120
|
+
## Key Concepts
|
|
121
|
+
|
|
122
|
+
- **Prices are in cents** (1–99). A price of 65 means $0.65 per share.
|
|
123
|
+
- **Outcomes are yes or no.** Each market is a binary question.
|
|
124
|
+
- **Read-only tools work with zero config.** Trading tools need a wallet — run `context_generate_wallet` first.
|
|
125
|
+
- **Shared config.** The MCP server and [CLI](https://github.com/contextwtf/context-cli) share `~/.config/context/config.env`, so you only set up once.
|
|
59
126
|
|
|
60
127
|
## Documentation
|
|
61
128
|
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
import { dirname, resolve } from "path";
|
|
4
|
+
import { loadConfig } from "./lib/config.js";
|
|
7
5
|
import { registerMarketTools } from "./tools/markets.js";
|
|
8
6
|
import { registerOrderTools } from "./tools/orders.js";
|
|
9
7
|
import { registerPortfolioTools } from "./tools/portfolio.js";
|
|
10
8
|
import { registerAccountTools } from "./tools/account.js";
|
|
11
9
|
import { registerQuestionTools } from "./tools/questions.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// Load keys from ~/.config/context/config.env (shared with the CLI).
|
|
11
|
+
// Env vars take precedence — config file fills in what's missing.
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
for (const [key, value] of Object.entries(config)) {
|
|
14
|
+
if (!process.env[key]) {
|
|
15
|
+
process.env[key] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
15
18
|
const server = new McpServer({
|
|
16
19
|
name: "context-markets",
|
|
17
|
-
version: "0.1
|
|
20
|
+
version: "0.2.1",
|
|
21
|
+
}, {
|
|
22
|
+
instructions: "Context Markets MCP — trade prediction markets from any AI agent.\n\n" +
|
|
23
|
+
"ONBOARDING: Before any trading operation, the user needs a wallet. " +
|
|
24
|
+
"If a trading tool fails with 'No wallet configured', guide the user through setup:\n" +
|
|
25
|
+
"1. Run context_generate_wallet to create or import a wallet.\n" +
|
|
26
|
+
"2. The user needs ETH on Base for gas fees — show them their address.\n" +
|
|
27
|
+
"3. Run context_account_setup to approve contracts.\n" +
|
|
28
|
+
"4. Deposit USDC with context_deposit.\n\n" +
|
|
29
|
+
"Read-only tools (context_list_markets, context_get_market, context_get_quotes, etc.) " +
|
|
30
|
+
"work without a wallet. Trading, portfolio, account, and market creation tools require a private key.\n\n" +
|
|
31
|
+
"IMPORTANT: Never generate a wallet or overwrite an existing one without explicit user confirmation.",
|
|
18
32
|
});
|
|
19
33
|
registerMarketTools(server);
|
|
20
34
|
registerOrderTools(server);
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { ContextClient } from "context-markets";
|
|
2
|
+
/** Reset the cached trading client so the next call picks up new env vars. */
|
|
3
|
+
export declare function resetTradingClient(): void;
|
|
2
4
|
export declare function getReadClient(): ContextClient;
|
|
3
5
|
export declare function getTradingClient(): ContextClient;
|
package/dist/lib/client.js
CHANGED
|
@@ -1,31 +1,49 @@
|
|
|
1
1
|
import { ContextClient } from "context-markets";
|
|
2
|
-
|
|
3
|
-
let
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
let readCache = null;
|
|
4
|
+
let tradingCache = null;
|
|
5
|
+
/** Reset the cached trading client so the next call picks up new env vars. */
|
|
6
|
+
export function resetTradingClient() {
|
|
7
|
+
tradingCache = null;
|
|
8
|
+
}
|
|
9
|
+
/** Resolve a key from env vars first, then ~/.config/context/config.env. */
|
|
10
|
+
function resolveKey(envKey) {
|
|
11
|
+
return process.env[envKey] || loadConfig()[envKey] || undefined;
|
|
12
|
+
}
|
|
4
13
|
function getChain() {
|
|
5
14
|
return process.env.CONTEXT_CHAIN === "testnet" ? "testnet" : "mainnet";
|
|
6
15
|
}
|
|
16
|
+
function cacheKey(chain, apiKey, privateKey) {
|
|
17
|
+
return `${chain}_${apiKey ?? ""}_${privateKey ?? ""}`;
|
|
18
|
+
}
|
|
7
19
|
export function getReadClient() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
const chain = getChain();
|
|
21
|
+
const apiKey = resolveKey("CONTEXT_API_KEY");
|
|
22
|
+
const key = cacheKey(chain, apiKey);
|
|
23
|
+
if (readCache?.key === key) {
|
|
24
|
+
return readCache.client;
|
|
13
25
|
}
|
|
14
|
-
|
|
26
|
+
const client = new ContextClient({ apiKey, chain });
|
|
27
|
+
readCache = { client, key };
|
|
28
|
+
return client;
|
|
15
29
|
}
|
|
16
30
|
export function getTradingClient() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
signer: { privateKey: privateKey },
|
|
28
|
-
});
|
|
31
|
+
const chain = getChain();
|
|
32
|
+
const apiKey = resolveKey("CONTEXT_API_KEY");
|
|
33
|
+
const privateKey = resolveKey("CONTEXT_PRIVATE_KEY");
|
|
34
|
+
if (!privateKey) {
|
|
35
|
+
throw new Error("No wallet configured. Run context_generate_wallet to create one, " +
|
|
36
|
+
"or set CONTEXT_PRIVATE_KEY in your environment.");
|
|
37
|
+
}
|
|
38
|
+
const key = cacheKey(chain, apiKey, privateKey);
|
|
39
|
+
if (tradingCache?.key === key) {
|
|
40
|
+
return tradingCache.client;
|
|
29
41
|
}
|
|
30
|
-
|
|
42
|
+
const client = new ContextClient({
|
|
43
|
+
apiKey,
|
|
44
|
+
chain,
|
|
45
|
+
signer: { privateKey: privateKey },
|
|
46
|
+
});
|
|
47
|
+
tradingCache = { client, key };
|
|
48
|
+
return client;
|
|
31
49
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Display-friendly path to the config file. */
|
|
2
|
+
export declare function configPath(): string;
|
|
3
|
+
/** Load config from ~/.config/context/config.env. Returns {} if missing. */
|
|
4
|
+
export declare function loadConfig(): Record<string, string>;
|
|
5
|
+
/** Save values to ~/.config/context/config.env, merging with existing. chmod 600. */
|
|
6
|
+
export declare function saveConfig(values: Record<string, string>): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".config", "context");
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.env");
|
|
6
|
+
/** Display-friendly path to the config file. */
|
|
7
|
+
export function configPath() {
|
|
8
|
+
return "~/.config/context/config.env";
|
|
9
|
+
}
|
|
10
|
+
/** Parse a KEY=VALUE env file, stripping comments, blanks, and surrounding quotes. */
|
|
11
|
+
function parseEnvFile(content) {
|
|
12
|
+
const entries = {};
|
|
13
|
+
for (const line of content.split("\n")) {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
16
|
+
continue;
|
|
17
|
+
const eq = trimmed.indexOf("=");
|
|
18
|
+
if (eq === -1)
|
|
19
|
+
continue;
|
|
20
|
+
const key = trimmed.slice(0, eq);
|
|
21
|
+
let value = trimmed.slice(eq + 1);
|
|
22
|
+
// Strip surrounding quotes
|
|
23
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
24
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
25
|
+
value = value.slice(1, -1);
|
|
26
|
+
}
|
|
27
|
+
entries[key] = value;
|
|
28
|
+
}
|
|
29
|
+
return entries;
|
|
30
|
+
}
|
|
31
|
+
/** Serialize key-value pairs as KEY="VALUE" lines. */
|
|
32
|
+
function serializeEnvFile(data) {
|
|
33
|
+
return Object.entries(data)
|
|
34
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
35
|
+
.join("\n") + "\n";
|
|
36
|
+
}
|
|
37
|
+
/** Load config from ~/.config/context/config.env. Returns {} if missing. */
|
|
38
|
+
export function loadConfig() {
|
|
39
|
+
try {
|
|
40
|
+
return parseEnvFile(readFileSync(CONFIG_FILE, "utf-8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Save values to ~/.config/context/config.env, merging with existing. chmod 600. */
|
|
47
|
+
export function saveConfig(values) {
|
|
48
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
49
|
+
const existing = loadConfig();
|
|
50
|
+
const merged = { ...existing, ...values };
|
|
51
|
+
writeFileSync(CONFIG_FILE, serializeEnvFile(merged), { mode: 0o600 });
|
|
52
|
+
}
|
package/dist/lib/utils.d.ts
CHANGED
package/dist/lib/utils.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export function validateHexNonce(nonce) {
|
|
2
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(nonce)) {
|
|
3
|
+
throw new Error("Invalid nonce format: expected 0x-prefixed 32-byte hex string (66 characters)");
|
|
4
|
+
}
|
|
5
|
+
return nonce;
|
|
6
|
+
}
|
|
1
7
|
export function toolResult(data) {
|
|
2
8
|
return {
|
|
3
9
|
content: [
|
package/dist/tools/account.js
CHANGED
|
@@ -1,9 +1,141 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
3
|
+
import { formatEther, formatUnits } from "viem";
|
|
4
|
+
import { getTradingClient, resetTradingClient } from "../lib/client.js";
|
|
5
|
+
import { loadConfig, saveConfig, configPath } from "../lib/config.js";
|
|
3
6
|
import { toolResult, toolError } from "../lib/utils.js";
|
|
7
|
+
const MIN_ETH_FOR_GAS = 1000000000000n; // 0.000001 ETH
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Tool registration
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
4
11
|
export function registerAccountTools(server) {
|
|
5
|
-
// 1.
|
|
6
|
-
server.tool("
|
|
12
|
+
// 1. Generate or import a wallet
|
|
13
|
+
server.tool("context_generate_wallet", "Generate a new trading wallet or import an existing private key. " +
|
|
14
|
+
"Saves credentials to ~/.config/context/config.env (shared with the Context CLI). " +
|
|
15
|
+
"Also saves the API key if provided. " +
|
|
16
|
+
"IMPORTANT: If a wallet already exists, this will tell you — it will NOT overwrite " +
|
|
17
|
+
"unless you explicitly pass overwrite: true. Always confirm with the user before overwriting.", {
|
|
18
|
+
privateKey: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe("Import an existing private key (0x-prefixed hex). Omit to generate a new one.")
|
|
21
|
+
.optional(),
|
|
22
|
+
apiKey: z
|
|
23
|
+
.string()
|
|
24
|
+
.describe("Context API key (ctx_...). Optional but recommended — get one at context.markets.")
|
|
25
|
+
.optional(),
|
|
26
|
+
overwrite: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.describe("Set to true to replace an existing wallet. The agent MUST confirm with the user first.")
|
|
29
|
+
.optional(),
|
|
30
|
+
}, async (params) => {
|
|
31
|
+
try {
|
|
32
|
+
const existingConfig = loadConfig();
|
|
33
|
+
const existingKey = process.env.CONTEXT_PRIVATE_KEY || existingConfig.CONTEXT_PRIVATE_KEY;
|
|
34
|
+
const hasExistingWallet = Boolean(existingKey);
|
|
35
|
+
if (hasExistingWallet && params.privateKey && !params.overwrite) {
|
|
36
|
+
return toolError("A wallet is already configured. Set overwrite: true to replace it. WARNING: This will replace your existing wallet permanently.");
|
|
37
|
+
}
|
|
38
|
+
if (existingKey && !params.overwrite) {
|
|
39
|
+
if (params.apiKey) {
|
|
40
|
+
saveConfig({ CONTEXT_API_KEY: params.apiKey });
|
|
41
|
+
process.env.CONTEXT_API_KEY = params.apiKey;
|
|
42
|
+
}
|
|
43
|
+
const account = privateKeyToAccount(existingKey);
|
|
44
|
+
return toolResult({
|
|
45
|
+
status: params.apiKey ? "api_key_saved" : "wallet_exists",
|
|
46
|
+
address: account.address,
|
|
47
|
+
configPath: configPath(),
|
|
48
|
+
saved: Boolean(params.apiKey),
|
|
49
|
+
message: params.apiKey
|
|
50
|
+
? "API key saved. Existing wallet left unchanged."
|
|
51
|
+
: "A wallet is already configured. Use context_wallet_status for full details. " +
|
|
52
|
+
"To replace it, call this tool again with overwrite: true — but confirm with the user first, " +
|
|
53
|
+
"as the old key will be lost.",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Validate imported key
|
|
57
|
+
let key;
|
|
58
|
+
if (params.privateKey) {
|
|
59
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(params.privateKey)) {
|
|
60
|
+
return toolError("Invalid private key format. Expected 0x-prefixed 64-character hex string.");
|
|
61
|
+
}
|
|
62
|
+
key = params.privateKey;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
key = generatePrivateKey();
|
|
66
|
+
}
|
|
67
|
+
const account = privateKeyToAccount(key);
|
|
68
|
+
// Build config to save
|
|
69
|
+
const toSave = { CONTEXT_PRIVATE_KEY: key };
|
|
70
|
+
if (params.apiKey) {
|
|
71
|
+
toSave.CONTEXT_API_KEY = params.apiKey;
|
|
72
|
+
process.env.CONTEXT_API_KEY = params.apiKey;
|
|
73
|
+
}
|
|
74
|
+
// Save to shared config file (chmod 600)
|
|
75
|
+
saveConfig(toSave);
|
|
76
|
+
process.env.CONTEXT_PRIVATE_KEY = key;
|
|
77
|
+
resetTradingClient();
|
|
78
|
+
return toolResult({
|
|
79
|
+
status: params.privateKey ? "imported" : "generated",
|
|
80
|
+
address: account.address,
|
|
81
|
+
saved: true,
|
|
82
|
+
configPath: configPath(),
|
|
83
|
+
message: params.privateKey
|
|
84
|
+
? "Wallet imported and saved. Private key stored securely in config file."
|
|
85
|
+
: "Wallet generated and saved. Private key stored securely in config file.",
|
|
86
|
+
nextSteps: [
|
|
87
|
+
"Fund the wallet with ETH on Base for gas fees.",
|
|
88
|
+
"Run context_account_setup to approve contracts for trading.",
|
|
89
|
+
"Deposit USDC with context_deposit to start trading.",
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return toolError(error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// 2. Full wallet status
|
|
98
|
+
server.tool("context_wallet_status", "Get comprehensive wallet status: address, ETH balance (for gas), USDC balance, " +
|
|
99
|
+
"approval status, and whether the account is ready to trade. " +
|
|
100
|
+
"Requires a wallet — run context_generate_wallet first if not set up.", {}, async () => {
|
|
101
|
+
try {
|
|
102
|
+
const client = getTradingClient();
|
|
103
|
+
const [status, balance] = await Promise.all([
|
|
104
|
+
client.account.status(),
|
|
105
|
+
client.portfolio.balance(),
|
|
106
|
+
]);
|
|
107
|
+
const isReady = !status.needsApprovals;
|
|
108
|
+
const ethBalance = formatEther(status.ethBalance);
|
|
109
|
+
const usdcBalance = formatUnits(BigInt(balance.usdc.balance), 6);
|
|
110
|
+
const usdcAllowance = formatUnits(status.usdcAllowance, 6);
|
|
111
|
+
return toolResult({
|
|
112
|
+
address: status.address,
|
|
113
|
+
ethBalance,
|
|
114
|
+
usdcBalance,
|
|
115
|
+
usdcAllowance,
|
|
116
|
+
isReady,
|
|
117
|
+
needsApprovals: status.needsApprovals,
|
|
118
|
+
needsGaslessSetup: status.needsGaslessSetup,
|
|
119
|
+
isOperatorApproved: status.isOperatorApproved,
|
|
120
|
+
nextSteps: !isReady
|
|
121
|
+
? [
|
|
122
|
+
...(status.ethBalance < MIN_ETH_FOR_GAS
|
|
123
|
+
? [`Send ETH to ${status.address} on Base for gas fees.`]
|
|
124
|
+
: []),
|
|
125
|
+
"Run context_account_setup to approve contracts.",
|
|
126
|
+
"Run context_deposit to deposit USDC for trading.",
|
|
127
|
+
]
|
|
128
|
+
: ["Wallet is fully set up. You can start trading."],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return toolError(error);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
// 3. Set up trading account (approve contracts)
|
|
136
|
+
server.tool("context_account_setup", "Approve USDC spending and operator permissions for trading. " +
|
|
137
|
+
"Run this once before your first trade. Requires a wallet and ETH on Base for gas — " +
|
|
138
|
+
"run context_generate_wallet first if not set up.", {}, async () => {
|
|
7
139
|
try {
|
|
8
140
|
const client = getTradingClient();
|
|
9
141
|
const status = await client.account.status();
|
|
@@ -23,8 +155,51 @@ export function registerAccountTools(server) {
|
|
|
23
155
|
return toolError(error);
|
|
24
156
|
}
|
|
25
157
|
});
|
|
26
|
-
//
|
|
27
|
-
server.tool("
|
|
158
|
+
// 4. Deposit USDC
|
|
159
|
+
server.tool("context_deposit", "Deposit USDC into the exchange for trading. Requires approved contracts — " +
|
|
160
|
+
"run context_account_setup first if needed.", {
|
|
161
|
+
amount: z
|
|
162
|
+
.number()
|
|
163
|
+
.positive()
|
|
164
|
+
.describe("Amount of USDC to deposit"),
|
|
165
|
+
}, async (params) => {
|
|
166
|
+
try {
|
|
167
|
+
const client = getTradingClient();
|
|
168
|
+
const txHash = await client.account.deposit(params.amount);
|
|
169
|
+
return toolResult({
|
|
170
|
+
message: `Deposited $${params.amount.toFixed(2)} USDC successfully.`,
|
|
171
|
+
amount: params.amount,
|
|
172
|
+
txHash,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return toolError(error);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// 5. Withdraw USDC
|
|
180
|
+
server.tool("context_withdraw", "Withdraw USDC from the exchange back to your wallet. " +
|
|
181
|
+
"Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
182
|
+
amount: z
|
|
183
|
+
.number()
|
|
184
|
+
.positive()
|
|
185
|
+
.describe("Amount of USDC to withdraw"),
|
|
186
|
+
}, async (params) => {
|
|
187
|
+
try {
|
|
188
|
+
const client = getTradingClient();
|
|
189
|
+
const txHash = await client.account.withdraw(params.amount);
|
|
190
|
+
return toolResult({
|
|
191
|
+
message: `Withdrew $${params.amount.toFixed(2)} USDC successfully.`,
|
|
192
|
+
amount: params.amount,
|
|
193
|
+
txHash,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return toolError(error);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
// 6. Mint test USDC
|
|
201
|
+
server.tool("context_mint_test_usdc", "Mint test USDC on Base Sepolia testnet for paper trading. Default 1000 USDC. " +
|
|
202
|
+
"Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
28
203
|
amount: z
|
|
29
204
|
.number()
|
|
30
205
|
.describe("Amount of test USDC to mint (default 1000)")
|
package/dist/tools/orders.js
CHANGED
|
@@ -1,37 +1,95 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getTradingClient } from "../lib/client.js";
|
|
3
|
-
import { toolResult, toolError } from "../lib/utils.js";
|
|
3
|
+
import { toolResult, toolError, validateHexNonce, } from "../lib/utils.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Shared schemas and helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const outcomeSchema = z.enum(["yes", "no"]).describe("Which outcome token: yes or no");
|
|
8
|
+
const sideSchema = z.enum(["buy", "sell"]).describe("Buy to enter a position, sell to exit. Defaults to buy.").default("buy");
|
|
9
|
+
const priceSchema = z.number().min(1).max(99).describe("Price in cents (1-99)");
|
|
10
|
+
const sizeSchema = z.number().min(0.01).describe("Number of contracts (min 0.01)");
|
|
11
|
+
const expirySchema = z.number().positive().describe("Auto-expire the order after this many seconds").optional();
|
|
12
|
+
const inventoryModeSchema = z
|
|
13
|
+
.enum(["any", "hold", "mint"])
|
|
14
|
+
.describe("Token inventory mode. 'any' (default): use existing tokens or mint new ones. " +
|
|
15
|
+
"'hold': require existing token inventory. " +
|
|
16
|
+
"'mint': mint new complete sets from USDC (use for sells when you don't hold tokens).")
|
|
17
|
+
.optional();
|
|
18
|
+
const takerOnlySchema = z
|
|
19
|
+
.boolean()
|
|
20
|
+
.describe("If true, the order must fill immediately or be voided. Default false.")
|
|
21
|
+
.optional();
|
|
22
|
+
const INVENTORY_MODE_MAP = { any: 0, hold: 1, mint: 2 };
|
|
23
|
+
/** Build a PlaceOrderRequest from tool params. */
|
|
24
|
+
function buildOrderRequest(params) {
|
|
25
|
+
const req = {
|
|
26
|
+
marketId: params.marketId,
|
|
27
|
+
outcome: params.outcome,
|
|
28
|
+
side: params.side,
|
|
29
|
+
priceCents: params.priceCents,
|
|
30
|
+
size: params.size,
|
|
31
|
+
};
|
|
32
|
+
if (params.expirySeconds !== undefined)
|
|
33
|
+
req.expirySeconds = params.expirySeconds;
|
|
34
|
+
if (params.inventoryMode !== undefined) {
|
|
35
|
+
req.inventoryModeConstraint = INVENTORY_MODE_MAP[params.inventoryMode];
|
|
36
|
+
}
|
|
37
|
+
if (params.takerOnly) {
|
|
38
|
+
req.makerRoleConstraint = 2; // TAKER_ONLY
|
|
39
|
+
}
|
|
40
|
+
return req;
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Tool registration
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
4
45
|
export function registerOrderTools(server) {
|
|
5
|
-
// 1. Place
|
|
6
|
-
server.tool("context_place_order", "Place a buy order on a prediction market.
|
|
46
|
+
// 1. Place an order (buy or sell)
|
|
47
|
+
server.tool("context_place_order", "Place a buy or sell order on a prediction market. " +
|
|
48
|
+
"Prices in cents (1-99), size in contracts (min 0.01). " +
|
|
49
|
+
"Omit price for a market order. " +
|
|
50
|
+
"Note: Market orders (no price specified) will fill at any price up to 99 cents. " +
|
|
51
|
+
"For tighter price control, specify a limit price. " +
|
|
52
|
+
"Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
7
53
|
marketId: z.string().describe("The unique identifier of the market"),
|
|
8
|
-
|
|
9
|
-
|
|
54
|
+
outcome: outcomeSchema,
|
|
55
|
+
side: sideSchema,
|
|
56
|
+
size: sizeSchema,
|
|
10
57
|
price: z
|
|
11
58
|
.number()
|
|
59
|
+
.min(1)
|
|
60
|
+
.max(99)
|
|
12
61
|
.describe("Limit price in cents (1-99). Omit for a market order.")
|
|
13
62
|
.optional(),
|
|
63
|
+
expirySeconds: expirySchema,
|
|
64
|
+
inventoryMode: inventoryModeSchema,
|
|
65
|
+
takerOnly: takerOnlySchema,
|
|
14
66
|
}, async (params) => {
|
|
15
67
|
try {
|
|
16
68
|
const client = getTradingClient();
|
|
17
69
|
let result;
|
|
18
70
|
if (params.price !== undefined) {
|
|
19
|
-
result = await client.orders.create({
|
|
71
|
+
result = await client.orders.create(buildOrderRequest({
|
|
20
72
|
marketId: params.marketId,
|
|
21
|
-
outcome: params.
|
|
22
|
-
side:
|
|
73
|
+
outcome: params.outcome,
|
|
74
|
+
side: params.side,
|
|
23
75
|
priceCents: params.price,
|
|
24
76
|
size: params.size,
|
|
25
|
-
|
|
77
|
+
expirySeconds: params.expirySeconds,
|
|
78
|
+
inventoryMode: params.inventoryMode,
|
|
79
|
+
takerOnly: params.takerOnly,
|
|
80
|
+
}));
|
|
26
81
|
}
|
|
27
82
|
else {
|
|
28
|
-
|
|
83
|
+
const marketReq = {
|
|
29
84
|
marketId: params.marketId,
|
|
30
|
-
outcome: params.
|
|
31
|
-
side:
|
|
85
|
+
outcome: params.outcome,
|
|
86
|
+
side: params.side,
|
|
32
87
|
maxPriceCents: 99,
|
|
33
88
|
maxSize: params.size,
|
|
34
|
-
}
|
|
89
|
+
};
|
|
90
|
+
if (params.expirySeconds !== undefined)
|
|
91
|
+
marketReq.expirySeconds = params.expirySeconds;
|
|
92
|
+
result = await client.orders.createMarket(marketReq);
|
|
35
93
|
}
|
|
36
94
|
return toolResult(result);
|
|
37
95
|
}
|
|
@@ -40,20 +98,52 @@ export function registerOrderTools(server) {
|
|
|
40
98
|
}
|
|
41
99
|
});
|
|
42
100
|
// 2. Cancel an open order
|
|
43
|
-
server.tool("context_cancel_order", "Cancel an open order by its nonce. Requires
|
|
101
|
+
server.tool("context_cancel_order", "Cancel an open order by its nonce. Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
44
102
|
nonce: z.string().describe("The nonce of the order to cancel"),
|
|
45
103
|
}, async (params) => {
|
|
46
104
|
try {
|
|
47
105
|
const client = getTradingClient();
|
|
48
|
-
const result = await client.orders.cancel(params.nonce);
|
|
106
|
+
const result = await client.orders.cancel(validateHexNonce(params.nonce));
|
|
107
|
+
return toolResult(result);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return toolError(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// 3. Cancel and replace an order atomically
|
|
114
|
+
server.tool("context_cancel_replace_order", "Atomically cancel an existing order and place a new one. " +
|
|
115
|
+
"If either operation fails, both fail — you're never left without a position. " +
|
|
116
|
+
"Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
117
|
+
cancelNonce: z.string().describe("Hex nonce of the order to cancel"),
|
|
118
|
+
marketId: z.string().describe("The unique identifier of the market for the new order"),
|
|
119
|
+
outcome: outcomeSchema,
|
|
120
|
+
side: sideSchema,
|
|
121
|
+
priceCents: priceSchema,
|
|
122
|
+
size: sizeSchema,
|
|
123
|
+
expirySeconds: expirySchema,
|
|
124
|
+
inventoryMode: inventoryModeSchema,
|
|
125
|
+
takerOnly: takerOnlySchema,
|
|
126
|
+
}, async (params) => {
|
|
127
|
+
try {
|
|
128
|
+
const client = getTradingClient();
|
|
129
|
+
const result = await client.orders.cancelReplace(validateHexNonce(params.cancelNonce), buildOrderRequest({
|
|
130
|
+
marketId: params.marketId,
|
|
131
|
+
outcome: params.outcome,
|
|
132
|
+
side: params.side,
|
|
133
|
+
priceCents: params.priceCents,
|
|
134
|
+
size: params.size,
|
|
135
|
+
expirySeconds: params.expirySeconds,
|
|
136
|
+
inventoryMode: params.inventoryMode,
|
|
137
|
+
takerOnly: params.takerOnly,
|
|
138
|
+
}));
|
|
49
139
|
return toolResult(result);
|
|
50
140
|
}
|
|
51
141
|
catch (error) {
|
|
52
142
|
return toolError(error);
|
|
53
143
|
}
|
|
54
144
|
});
|
|
55
|
-
//
|
|
56
|
-
server.tool("context_my_orders", "List your open orders, optionally filtered to a specific market. Requires
|
|
145
|
+
// 4. List open orders
|
|
146
|
+
server.tool("context_my_orders", "List your open orders, optionally filtered to a specific market. Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
57
147
|
marketId: z
|
|
58
148
|
.string()
|
|
59
149
|
.describe("Filter orders to a specific market")
|
|
@@ -68,4 +158,88 @@ export function registerOrderTools(server) {
|
|
|
68
158
|
return toolError(error);
|
|
69
159
|
}
|
|
70
160
|
});
|
|
161
|
+
// 5. Bulk create orders
|
|
162
|
+
server.tool("context_bulk_create_orders", "Create multiple orders in a single atomic batch. " +
|
|
163
|
+
"All orders are submitted together — more efficient than placing them one by one. " +
|
|
164
|
+
"Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
165
|
+
orders: z.array(z.object({
|
|
166
|
+
marketId: z.string().describe("The unique identifier of the market"),
|
|
167
|
+
outcome: outcomeSchema,
|
|
168
|
+
side: sideSchema,
|
|
169
|
+
priceCents: priceSchema,
|
|
170
|
+
size: sizeSchema,
|
|
171
|
+
expirySeconds: expirySchema,
|
|
172
|
+
inventoryMode: inventoryModeSchema,
|
|
173
|
+
takerOnly: takerOnlySchema,
|
|
174
|
+
})).describe("Array of orders to create"),
|
|
175
|
+
}, async (params) => {
|
|
176
|
+
try {
|
|
177
|
+
const client = getTradingClient();
|
|
178
|
+
const requests = params.orders.map((o) => buildOrderRequest({
|
|
179
|
+
marketId: o.marketId,
|
|
180
|
+
outcome: o.outcome,
|
|
181
|
+
side: o.side,
|
|
182
|
+
priceCents: o.priceCents,
|
|
183
|
+
size: o.size,
|
|
184
|
+
expirySeconds: o.expirySeconds,
|
|
185
|
+
inventoryMode: o.inventoryMode,
|
|
186
|
+
takerOnly: o.takerOnly,
|
|
187
|
+
}));
|
|
188
|
+
const result = await client.orders.bulkCreate(requests);
|
|
189
|
+
return toolResult(result);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
return toolError(error);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
// 6. Bulk cancel orders
|
|
196
|
+
server.tool("context_bulk_cancel_orders", "Cancel multiple open orders in a single batch. " +
|
|
197
|
+
"Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
198
|
+
nonces: z.array(z.string()).describe("Array of hex nonces of orders to cancel"),
|
|
199
|
+
}, async (params) => {
|
|
200
|
+
try {
|
|
201
|
+
const client = getTradingClient();
|
|
202
|
+
const result = await client.orders.bulkCancel(params.nonces.map(validateHexNonce));
|
|
203
|
+
return toolResult(result);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return toolError(error);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// 7. Bulk mixed operations (create + cancel atomically)
|
|
210
|
+
server.tool("context_bulk_orders", "Atomically create and cancel orders in a single batch. " +
|
|
211
|
+
"Use this for quote updates — cancel stale orders and place new ones in one call. " +
|
|
212
|
+
"Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
213
|
+
creates: z.array(z.object({
|
|
214
|
+
marketId: z.string().describe("The unique identifier of the market"),
|
|
215
|
+
outcome: outcomeSchema,
|
|
216
|
+
side: sideSchema,
|
|
217
|
+
priceCents: priceSchema,
|
|
218
|
+
size: sizeSchema,
|
|
219
|
+
expirySeconds: expirySchema,
|
|
220
|
+
inventoryMode: inventoryModeSchema,
|
|
221
|
+
takerOnly: takerOnlySchema,
|
|
222
|
+
})).describe("Orders to create").optional(),
|
|
223
|
+
cancelNonces: z.array(z.string()).describe("Hex nonces of orders to cancel").optional(),
|
|
224
|
+
}, async (params) => {
|
|
225
|
+
try {
|
|
226
|
+
const client = getTradingClient();
|
|
227
|
+
const creates = (params.creates ?? []).map((o) => buildOrderRequest({
|
|
228
|
+
marketId: o.marketId,
|
|
229
|
+
outcome: o.outcome,
|
|
230
|
+
side: o.side,
|
|
231
|
+
priceCents: o.priceCents,
|
|
232
|
+
size: o.size,
|
|
233
|
+
expirySeconds: o.expirySeconds,
|
|
234
|
+
inventoryMode: o.inventoryMode,
|
|
235
|
+
takerOnly: o.takerOnly,
|
|
236
|
+
}));
|
|
237
|
+
const cancelNonces = (params.cancelNonces ?? []).map(validateHexNonce);
|
|
238
|
+
const result = await client.orders.bulk(creates, cancelNonces);
|
|
239
|
+
return toolResult(result);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
return toolError(error);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
71
245
|
}
|
package/dist/tools/portfolio.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getTradingClient } from "../lib/client.js";
|
|
|
3
3
|
import { toolResult, toolError } from "../lib/utils.js";
|
|
4
4
|
export function registerPortfolioTools(server) {
|
|
5
5
|
// 1. Get portfolio positions
|
|
6
|
-
server.tool("context_get_portfolio", "Get your prediction market positions with P&L. Filter by kind: all, active, won, lost, or claimable. Requires
|
|
6
|
+
server.tool("context_get_portfolio", "Get your prediction market positions with P&L. Filter by kind: all, active, won, lost, or claimable. Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
7
7
|
kind: z
|
|
8
8
|
.enum(["all", "active", "won", "lost", "claimable"])
|
|
9
9
|
.describe("Filter positions by kind")
|
|
@@ -21,7 +21,7 @@ export function registerPortfolioTools(server) {
|
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
23
|
// 2. Get balance
|
|
24
|
-
server.tool("context_get_balance", "Get your USDC balance (wallet + settlement) and outcome token holdings. Requires
|
|
24
|
+
server.tool("context_get_balance", "Get your USDC balance (wallet + settlement) and outcome token holdings. Requires a wallet — run context_generate_wallet first if not set up.", {}, async () => {
|
|
25
25
|
try {
|
|
26
26
|
const client = getTradingClient();
|
|
27
27
|
const result = await client.portfolio.balance();
|
package/dist/tools/questions.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getTradingClient } from "../lib/client.js";
|
|
|
3
3
|
import { toolResult, toolError } from "../lib/utils.js";
|
|
4
4
|
export function registerQuestionTools(server) {
|
|
5
5
|
// 1. Create a new prediction market from a question
|
|
6
|
-
server.tool("context_create_market", "Create a new prediction market from a natural language question. The AI oracle processes the question and generates a market. This may take 30-90 seconds. Requires
|
|
6
|
+
server.tool("context_create_market", "Create a new prediction market from a natural language question. The AI oracle processes the question and generates a market. This may take 30-90 seconds. Requires a wallet — run context_generate_wallet first if not set up.", {
|
|
7
7
|
question: z
|
|
8
8
|
.string()
|
|
9
9
|
.describe("A natural language question to create a prediction market for"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-markets-mcp",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MCP server for Context Markets — browse, trade, and create prediction markets from any AI agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"dev": "tsc && node dist/index.js"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"context-markets": "^0.
|
|
16
|
+
"context-markets": "^0.6.0",
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
18
|
-
"
|
|
18
|
+
"viem": "^2.47.5",
|
|
19
19
|
"zod": "^3.22.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|