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 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
- ## Available Tools
37
+ No environment variables needed — the server walks you through setup on first use.
38
+
39
+ ## Getting Started
42
40
 
43
- ### Read-only (no auth needed)
41
+ The first time you (or your agent) use a trading tool, the server will guide you through onboarding:
44
42
 
45
- `context_list_markets` · `context_get_market` · `context_get_quotes` · `context_get_orderbook` · `context_simulate_trade` · `context_price_history` · `context_get_oracle` · `context_global_activity`
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
- ### Trading (requires API key + private key)
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
- `context_place_order` · `context_cancel_order` · `context_my_orders` · `context_get_portfolio` · `context_get_balance` · `context_account_setup` · `context_mint_test_usdc` · `context_create_market`
51
+ ### Manual setup
50
52
 
51
- ## Environment Variables
53
+ If you prefer to configure manually:
52
54
 
53
- | Variable | Required | Description |
54
- |----------|----------|-------------|
55
- | `CONTEXT_API_KEY` | For all tools | API key from context.markets |
56
- | `CONTEXT_PRIVATE_KEY` | For trading only | Ethereum private key for signing |
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 tools work with zero config. Need an API key? Visit [context.markets](https://context.markets).
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 dotenv from "dotenv";
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
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = dirname(__filename);
14
- dotenv.config({ path: resolve(__dirname, "..", ".env") });
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.0",
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);
@@ -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;
@@ -1,31 +1,49 @@
1
1
  import { ContextClient } from "context-markets";
2
- let readClientInstance = null;
3
- let tradingClientInstance = null;
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
- if (!readClientInstance) {
9
- readClientInstance = new ContextClient({
10
- apiKey: process.env.CONTEXT_API_KEY,
11
- chain: getChain(),
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
- return readClientInstance;
26
+ const client = new ContextClient({ apiKey, chain });
27
+ readCache = { client, key };
28
+ return client;
15
29
  }
16
30
  export function getTradingClient() {
17
- if (!tradingClientInstance) {
18
- const apiKey = process.env.CONTEXT_API_KEY;
19
- const privateKey = process.env.CONTEXT_PRIVATE_KEY;
20
- if (!privateKey) {
21
- throw new Error("CONTEXT_PRIVATE_KEY is required for trading operations. " +
22
- "Set it in your MCP server env config.");
23
- }
24
- tradingClientInstance = new ContextClient({
25
- apiKey,
26
- chain: getChain(),
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
- return tradingClientInstance;
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
+ }
@@ -1,4 +1,5 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function validateHexNonce(nonce: string): `0x${string}`;
2
3
  export declare function toolResult(data: unknown): {
3
4
  content: {
4
5
  type: "text";
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: [
@@ -1,9 +1,141 @@
1
1
  import { z } from "zod";
2
- import { getTradingClient } from "../lib/client.js";
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. Set up trading account
6
- server.tool("context_account_setup", "Set up your trading account approves USDC spending and operator permissions. Run this before your first trade. Requires CONTEXT_PRIVATE_KEY.", {}, async () => {
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
- // 2. Mint test USDC
27
- server.tool("context_mint_test_usdc", "Mint test USDC on Base Sepolia testnet for paper trading. Default 1000 USDC. Requires CONTEXT_PRIVATE_KEY.", {
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)")
@@ -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 a buy order
6
- server.tool("context_place_order", "Place a buy order on a prediction market. Requires CONTEXT_PRIVATE_KEY. Prices in cents (1-99), size in contracts (min 0.01).", {
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
- side: z.enum(["yes", "no"]).describe("Which outcome to buy: yes or no"),
9
- size: z.number().describe("Number of contracts to buy (min 0.01)"),
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.side,
22
- side: "buy",
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
- result = await client.orders.createMarket({
83
+ const marketReq = {
29
84
  marketId: params.marketId,
30
- outcome: params.side,
31
- side: "buy",
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 CONTEXT_PRIVATE_KEY.", {
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
- // 3. List open orders
56
- server.tool("context_my_orders", "List your open orders, optionally filtered to a specific market. Requires CONTEXT_PRIVATE_KEY.", {
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
  }
@@ -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 CONTEXT_PRIVATE_KEY.", {
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 CONTEXT_PRIVATE_KEY.", {}, async () => {
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();
@@ -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 CONTEXT_PRIVATE_KEY.", {
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.2",
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.5.1",
16
+ "context-markets": "^0.6.0",
17
17
  "@modelcontextprotocol/sdk": "^1.0.0",
18
- "dotenv": "^16.3.1",
18
+ "viem": "^2.47.5",
19
19
  "zod": "^3.22.0"
20
20
  },
21
21
  "devDependencies": {