@unhedged/mcp 0.1.2

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +48 -0
  5. package/dist/client/errors.d.ts +15 -0
  6. package/dist/client/errors.js +40 -0
  7. package/dist/client/http.d.ts +21 -0
  8. package/dist/client/http.js +132 -0
  9. package/dist/client/types.d.ts +140 -0
  10. package/dist/client/types.js +6 -0
  11. package/dist/config.d.ts +7 -0
  12. package/dist/config.js +27 -0
  13. package/dist/index.d.ts +6 -0
  14. package/dist/index.js +4 -0
  15. package/dist/init/index.d.ts +10 -0
  16. package/dist/init/index.js +137 -0
  17. package/dist/init/snippet.d.ts +35 -0
  18. package/dist/init/snippet.js +72 -0
  19. package/dist/prompts/frontmatter.d.ts +28 -0
  20. package/dist/prompts/frontmatter.js +113 -0
  21. package/dist/prompts/index.d.ts +10 -0
  22. package/dist/prompts/index.js +50 -0
  23. package/dist/prompts/loader.d.ts +13 -0
  24. package/dist/prompts/loader.js +36 -0
  25. package/dist/safety/index.d.ts +1 -0
  26. package/dist/safety/index.js +1 -0
  27. package/dist/safety/spend-cap.d.ts +33 -0
  28. package/dist/safety/spend-cap.js +64 -0
  29. package/dist/server.d.ts +3 -0
  30. package/dist/server.js +75 -0
  31. package/dist/tools/balance.d.ts +2 -0
  32. package/dist/tools/balance.js +175 -0
  33. package/dist/tools/bets.d.ts +2 -0
  34. package/dist/tools/bets.js +109 -0
  35. package/dist/tools/index.d.ts +4 -0
  36. package/dist/tools/index.js +17 -0
  37. package/dist/tools/market-maker.d.ts +2 -0
  38. package/dist/tools/market-maker.js +38 -0
  39. package/dist/tools/markets.d.ts +2 -0
  40. package/dist/tools/markets.js +120 -0
  41. package/dist/tools/orders.d.ts +2 -0
  42. package/dist/tools/orders.js +256 -0
  43. package/dist/tools/portfolio.d.ts +2 -0
  44. package/dist/tools/portfolio.js +34 -0
  45. package/dist/tools/types.d.ts +44 -0
  46. package/dist/tools/types.js +10 -0
  47. package/package.json +54 -0
  48. package/skills/cash-out.md +51 -0
  49. package/skills/market-creator-assistant.md +45 -0
  50. package/skills/market-maker-workflow.md +58 -0
  51. package/skills/market-research.md +42 -0
  52. package/skills/place-hedged-position.md +51 -0
  53. package/skills/portfolio-review.md +34 -0
package/dist/server.js ADDED
@@ -0,0 +1,75 @@
1
+ import { createRequire } from 'node:module';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { UnhedgedHttpClient } from './client/http.js';
5
+ import { UnhedgedApiError } from './client/errors.js';
6
+ import { loadConfig } from './config.js';
7
+ import { getAllTools } from './tools/index.js';
8
+ import { SpendCapTracker, SpendCapExceededError } from './safety/spend-cap.js';
9
+ import { registerSkills } from './prompts/index.js';
10
+ const SERVER_NAME = 'unhedged-mcp';
11
+ // package.json sits one level above both src/ and dist/, so this resolves in
12
+ // dev (tsx) and installed builds alike.
13
+ const SERVER_VERSION = createRequire(import.meta.url)('../package.json').version;
14
+ export async function createServer() {
15
+ const config = loadConfig();
16
+ const client = new UnhedgedHttpClient(config);
17
+ const spendCap = new SpendCapTracker(config.maxSpendPerSession);
18
+ const context = { client, config, spendCap };
19
+ const server = new McpServer({
20
+ name: SERVER_NAME,
21
+ version: SERVER_VERSION,
22
+ });
23
+ for (const tool of getAllTools()) {
24
+ server.registerTool(tool.name, {
25
+ title: tool.title,
26
+ description: tool.description,
27
+ inputSchema: tool.inputSchema,
28
+ annotations: tool.annotations,
29
+ }, async (args) => {
30
+ try {
31
+ const result = await tool.handler(args, context);
32
+ return {
33
+ content: [
34
+ {
35
+ type: 'text',
36
+ text: JSON.stringify(result, null, 2),
37
+ },
38
+ ],
39
+ };
40
+ }
41
+ catch (err) {
42
+ const message = err instanceof UnhedgedApiError
43
+ ? err.toAgentMessage(tool.annotations.readOnlyHint !== true)
44
+ : err instanceof SpendCapExceededError
45
+ ? err.message
46
+ : err instanceof Error
47
+ ? err.message
48
+ : String(err);
49
+ return {
50
+ isError: true,
51
+ content: [{ type: 'text', text: message }],
52
+ };
53
+ }
54
+ });
55
+ }
56
+ // Skills (MCP prompts). Failure to load is non-fatal — log to stderr and continue
57
+ // with tools only.
58
+ try {
59
+ const registered = registerSkills(server);
60
+ process.stderr.write(`[unhedged-mcp] Registered ${registered.length} skills: ${registered.join(', ')}\n`);
61
+ }
62
+ catch (err) {
63
+ const msg = err instanceof Error ? err.message : String(err);
64
+ process.stderr.write(`[unhedged-mcp] Failed to register skills: ${msg}\n`);
65
+ }
66
+ if (config.maxSpendPerSession !== null) {
67
+ process.stderr.write(`[unhedged-mcp] Session cap: ${config.maxSpendPerSession} USD in bet stakes and buy-order notional. Excludes fees and withdrawals; resets on restart.\n`);
68
+ }
69
+ return server;
70
+ }
71
+ export async function startStdioServer() {
72
+ const server = await createServer();
73
+ const transport = new StdioServerTransport();
74
+ await server.connect(transport);
75
+ }
@@ -0,0 +1,2 @@
1
+ import { type AnyToolDefinition } from './types.js';
2
+ export declare const balanceTools: AnyToolDefinition[];
@@ -0,0 +1,175 @@
1
+ import { z } from 'zod';
2
+ import { defineTool, READ_ONLY } from './types.js';
3
+ import { UnhedgedApiError } from '../client/errors.js';
4
+ const GET_DESCRIPTION = `
5
+ Read the user's Unhedged **platform balance** (the off-chain, CEX-style balance used for betting and order placement — distinct from their on-chain wallet balance). Platform balances and fees are denominated in **USD**. Held balances \`ccAvailable\` and \`ccLocked\` are in **CC**. \`ccPriceUsd\` is the USD price of one CC.
6
+
7
+ Returns:
8
+ \`\`\`
9
+ {
10
+ balance: {
11
+ available, // free to bet/order/withdraw
12
+ lockedBets, // tied up in open parimutuel bets
13
+ lockedEngine, // tied up in resting CLOB orders
14
+ lockedWithdraws, // pending on-chain withdrawals
15
+ total, // sum of the above
16
+ totalDeposited,
17
+ totalWithdrawn,
18
+ ccAvailable,
19
+ ccLocked
20
+ },
21
+ withdrawalFee, // flat fee per withdrawal request (USD)
22
+ withdrawalMinAmount, // minimum withdrawal size (USD)
23
+ bettingFee, // current parimutuel fee (USD)
24
+ ccPriceUsd // current CC↔USD reference, or null if unknown
25
+ }
26
+ \`\`\`
27
+
28
+ All numeric fields except \`ccPriceUsd\` (a number or null) are returned as **strings** to preserve precision. Parse with \`Number()\` or a Decimal library as appropriate.
29
+
30
+ Requires scope: \`balance:read\`.
31
+
32
+ ### When to use
33
+ Before placing a bet or order, to confirm the user has the funds. After a trade, to show the new balance. Always **before** offering withdrawal options.
34
+ `.trim();
35
+ const WITHDRAW_DESCRIPTION = `
36
+ Request a withdrawal from the user's platform balance to their on-chain wallet. The platform locks the amount, queues a withdrawal transaction, and settles it asynchronously.
37
+
38
+ Required parameters:
39
+ - **amount**: Decimal **USD** amount to debit from the platform balance. Send a number for the default Canton flow or a decimal string for Ethereum USDC. In the Canton flow, the USD amount is swapped to CC at the current rate (see \`ccPriceUsd\` in \`get_balance\`) before settling to the wallet — the user receives CC, not a face-value USD token.
40
+
41
+ Optional:
42
+ - **network**: \`"ETHEREUM"\` routes the withdrawal to Ethereum Mainnet. If omitted, the existing Canton flow is used.
43
+ - **destinationAddress**: Ethereum destination address. Required when \`network\` is \`"ETHEREUM"\`.
44
+ - **destinationPartyId**: Canton party ID for the destination. Required for web2 users without a linked Canton wallet.
45
+ - **target**: \`"cc"\` (default) for the existing Canton flow.
46
+
47
+ Fees:
48
+ - **Canton**: a flat withdrawal fee (USD) is deducted on top of the amount (see \`withdrawalFee\` in \`get_balance\`).
49
+ - **Ethereum**: a dynamic network fee applies. The tool automatically fetches a fee quote (\`GET /api/v1/balance/ethereum/withdraw-quote\`) and locks it into the withdrawal request — no extra input is needed. The quote is short-lived (roughly 30–120 seconds) and is consumed by the withdrawal, so it is fetched fresh on every call. If quoting fails (e.g. amount out of limits, invalid destination, fee configuration unavailable), the tool errors out and no withdrawal is submitted.
50
+
51
+ Returns:
52
+ \`\`\`
53
+ {
54
+ withdrawal: { id, amount, fee, totalDeducted, status, createdAt },
55
+ message: "Withdrawal request submitted..."
56
+ }
57
+ \`\`\`
58
+
59
+ The MCP server adds an \`Idempotency-Key\` automatically.
60
+
61
+ Requires scope: \`balance:withdraw\`. **This scope is opt-in.** Keys minted without it can browse, bet, and trade but cannot move funds off the platform. Ethereum withdrawals additionally need \`balance:read\` on the key (for the fee-quote request).
62
+
63
+ ### Safety
64
+ - Withdrawals are rate-limited server-side (typically 5 per minute per user).
65
+
66
+ ### Examples
67
+ Withdraw $50 from the platform balance to the user's linked Canton wallet (delivered as CC at the current rate):
68
+ \`\`\`json
69
+ { "amount": 50 }
70
+ \`\`\`
71
+
72
+ Debit $25.50 from the platform balance and send the remainder after fees as USDC on Ethereum Mainnet:
73
+ \`\`\`json
74
+ { "amount": "25.5", "network": "ETHEREUM", "destinationAddress": "0x1111111111111111111111111111111111111111" }
75
+ \`\`\`
76
+ `.trim();
77
+ const decimalAmount = z.union([
78
+ z.number().positive(),
79
+ z.string().regex(/^(?:0|[1-9]\d*)(?:\.\d+)?$/).refine(value => /[1-9]/.test(value), 'Amount must be positive'),
80
+ ]);
81
+ const withdrawSchema = {
82
+ amount: decimalAmount.describe('Withdrawal amount in USD. Ethereum USDC accepts at most six decimal places.'),
83
+ network: z
84
+ .enum(['CANTON', 'ETHEREUM'])
85
+ .optional()
86
+ .describe('Withdrawal network. Omit this field to preserve the Canton default.'),
87
+ destinationAddress: z
88
+ .string()
89
+ .regex(/^0x[0-9a-fA-F]{40}$/)
90
+ .optional()
91
+ .describe('Ethereum destination address. Required when network is "ETHEREUM".'),
92
+ destinationPartyId: z
93
+ .string()
94
+ .optional()
95
+ .describe('Canton party ID. Required for web2 users with no linked Canton wallet.'),
96
+ target: z
97
+ .enum(['cc'])
98
+ .optional()
99
+ .describe('Canton asset target. Existing callers can omit this field.'),
100
+ };
101
+ export const balanceTools = [
102
+ defineTool({
103
+ name: 'get_balance',
104
+ title: 'Get platform balance',
105
+ description: GET_DESCRIPTION,
106
+ inputSchema: {},
107
+ annotations: READ_ONLY,
108
+ handler: async (_args, ctx) => {
109
+ return ctx.client.request({
110
+ method: 'GET',
111
+ path: '/api/v1/balance',
112
+ });
113
+ },
114
+ }),
115
+ defineTool({
116
+ name: 'request_withdrawal',
117
+ title: 'Request a withdrawal',
118
+ description: WITHDRAW_DESCRIPTION,
119
+ inputSchema: withdrawSchema,
120
+ // Moves funds off the platform to an agent-supplied destination.
121
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
122
+ handler: async (args, ctx) => {
123
+ if (args.network === 'ETHEREUM' && !args.destinationAddress) {
124
+ throw new Error('destinationAddress is required for an Ethereum withdrawal');
125
+ }
126
+ if (args.network !== 'ETHEREUM' && args.destinationAddress) {
127
+ throw new Error('network must be "ETHEREUM" when destinationAddress is provided');
128
+ }
129
+ if (args.network === 'ETHEREUM'
130
+ && (typeof args.amount !== 'string'
131
+ || !/^(?:0|[1-9]\d*)(?:\.\d{1,6})?$/.test(args.amount)
132
+ || !/[1-9]/.test(args.amount))) {
133
+ throw new Error('Ethereum amount must be a positive decimal string with at most six decimal places');
134
+ }
135
+ if (args.network === 'ETHEREUM') {
136
+ const quoteResponse = await ctx.client.request({
137
+ method: 'GET',
138
+ path: '/api/v1/balance/ethereum/withdraw-quote',
139
+ query: {
140
+ usdAmount: String(args.amount),
141
+ destinationAddress: args.destinationAddress,
142
+ },
143
+ }).catch((error) => {
144
+ if (error instanceof UnhedgedApiError) {
145
+ throw new Error(`No withdrawal was submitted. ${error.toAgentMessage()}`);
146
+ }
147
+ throw error;
148
+ });
149
+ const quoteId = quoteResponse?.quote?.quoteId;
150
+ if (typeof quoteId !== 'string' || quoteId.length === 0) {
151
+ throw new Error('Ethereum withdrawal quote response did not include a quoteId');
152
+ }
153
+ return ctx.client.request({
154
+ method: 'POST',
155
+ path: '/api/v1/balance/withdraw/ethereum',
156
+ body: {
157
+ amount: String(args.amount),
158
+ destinationAddress: args.destinationAddress,
159
+ quoteId,
160
+ },
161
+ });
162
+ }
163
+ const cantonBody = { amount: args.amount };
164
+ if (args.destinationPartyId)
165
+ cantonBody.destinationPartyId = args.destinationPartyId;
166
+ if (args.target)
167
+ cantonBody.target = args.target;
168
+ return ctx.client.request({
169
+ method: 'POST',
170
+ path: '/api/v1/balance/withdraw',
171
+ body: cantonBody,
172
+ });
173
+ },
174
+ }),
175
+ ];
@@ -0,0 +1,2 @@
1
+ import { type AnyToolDefinition } from './types.js';
2
+ export declare const betTools: AnyToolDefinition[];
@@ -0,0 +1,109 @@
1
+ import { z } from 'zod';
2
+ import { defineTool, READ_ONLY } from './types.js';
3
+ import { UnhedgedApiError } from '../client/errors.js';
4
+ const LIST_DESCRIPTION = `
5
+ List the user's **parimutuel bets**. (Order-book trades show up under \`list_orders\` instead.)
6
+
7
+ Filters:
8
+ - **status**: \`PENDING | CONFIRMED | FAILED | REFUNDED | WON | LOST\`. Omit to return any status.
9
+ - **marketId**: restrict to a single market.
10
+ - **limit**: 1-100, default 20.
11
+ - **offset**: pagination, default 0.
12
+
13
+ Returns \`{ bets, total }\`. Each bet has \`id\`, \`marketId\`, \`outcomeIndex\`, \`amount\` (string, USD), \`status\`, \`timeWeight\`, \`createdAt\`.
14
+
15
+ Requires scope: \`bet:read\`.
16
+
17
+ ### Examples
18
+ All bets on a specific market:
19
+ \`\`\`json
20
+ { "marketId": "clxabc123..." }
21
+ \`\`\`
22
+
23
+ Only winning bets, most recent first:
24
+ \`\`\`json
25
+ { "status": "WON", "limit": 50 }
26
+ \`\`\`
27
+ `.trim();
28
+ const PLACE_DESCRIPTION = `
29
+ Place a **parimutuel bet** on a market outcome. This is the simple "pool" market type — the user picks an outcome, commits USD from their platform balance, and shares the winning pool pro-rata if their outcome resolves. For order-book (CLOB) markets, use \`place_order\` instead.
30
+
31
+ Required parameters:
32
+ - **marketId**: ID of the market to bet on.
33
+ - **outcomeIndex**: zero-indexed outcome. Look up valid outcomes via \`get_market\` first; passing an out-of-range index will be rejected.
34
+ - **amount**: Stake in USD, at least the market's minimum bet (typically $1). The fee is charged on top: stake plus fee must fit within the user's available platform balance.
35
+
36
+ The MCP server attaches an \`Idempotency-Key\` header, but deliberately does **not** retry an ambiguous failure automatically. Do **not** immediately re-invoke this tool after a network error or timeout — a new call uses a new key and can place a duplicate bet. Check \`list_bets\` for the result. If the outcome remains uncertain, stop and ask the user before retrying.
37
+
38
+ Returns:
39
+ \`\`\`
40
+ { bet: { id, marketId, outcomeIndex, amount, status }, balanceAfter, fee }
41
+ \`\`\`
42
+
43
+ Requires scope: \`bet:place\`.
44
+
45
+ ### Safety
46
+ - If a **session spend cap** is configured, the \`amount\` adds to the running total; once exceeded, further calls refuse until the server restarts or the cap is raised.
47
+
48
+ ### Example
49
+ Bet $10 on outcome 0 (typically YES on a binary market):
50
+ \`\`\`json
51
+ { "marketId": "clxabc123...", "outcomeIndex": 0, "amount": 10 }
52
+ \`\`\`
53
+ `.trim();
54
+ const listSchema = {
55
+ status: z.enum(['PENDING', 'CONFIRMED', 'FAILED', 'REFUNDED', 'WON', 'LOST']).optional(),
56
+ marketId: z.string().optional(),
57
+ limit: z.number().int().min(1).max(100).optional(),
58
+ offset: z.number().int().min(0).optional(),
59
+ };
60
+ const placeSchema = {
61
+ marketId: z.string().min(1).describe('Market ID to bet on.'),
62
+ outcomeIndex: z.number().int().min(0).describe('Zero-indexed outcome to bet on.'),
63
+ amount: z
64
+ .number()
65
+ .positive()
66
+ .describe('USD amount to commit. Must be within available platform balance.'),
67
+ };
68
+ export const betTools = [
69
+ defineTool({
70
+ name: 'list_bets',
71
+ title: 'List user bets',
72
+ description: LIST_DESCRIPTION,
73
+ inputSchema: listSchema,
74
+ annotations: READ_ONLY,
75
+ handler: async (args, ctx) => {
76
+ return ctx.client.request({
77
+ method: 'GET',
78
+ path: '/api/v1/bets',
79
+ query: args,
80
+ });
81
+ },
82
+ }),
83
+ defineTool({
84
+ name: 'place_bet',
85
+ title: 'Place a parimutuel bet',
86
+ description: PLACE_DESCRIPTION,
87
+ inputSchema: placeSchema,
88
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
89
+ handler: async (args, ctx) => {
90
+ ctx.spendCap.reserve(args.amount, 'place_bet');
91
+ try {
92
+ return await ctx.client.request({
93
+ method: 'POST',
94
+ path: '/api/v1/bets',
95
+ body: { marketId: args.marketId, outcomeIndex: args.outcomeIndex, amount: args.amount },
96
+ });
97
+ }
98
+ catch (err) {
99
+ // A 4xx means the server definitively rejected the bet, so nothing was
100
+ // spent — give the reservation back. Ambiguous outcomes (network
101
+ // errors, 5xx) keep it, erring on the side of capping too much.
102
+ if (err instanceof UnhedgedApiError && err.status >= 400 && err.status < 500) {
103
+ ctx.spendCap.release(args.amount);
104
+ }
105
+ throw err;
106
+ }
107
+ },
108
+ }),
109
+ ];
@@ -0,0 +1,4 @@
1
+ import type { AnyToolDefinition } from './types.js';
2
+ export declare function getAllTools(): AnyToolDefinition[];
3
+ export { defineTool } from './types.js';
4
+ export type { AnyToolDefinition, ToolDefinition, ToolContext } from './types.js';
@@ -0,0 +1,17 @@
1
+ import { marketTools } from './markets.js';
2
+ import { balanceTools } from './balance.js';
3
+ import { portfolioTools } from './portfolio.js';
4
+ import { betTools } from './bets.js';
5
+ import { orderTools } from './orders.js';
6
+ import { marketMakerTools } from './market-maker.js';
7
+ export function getAllTools() {
8
+ return [
9
+ ...marketTools,
10
+ ...balanceTools,
11
+ ...portfolioTools,
12
+ ...betTools,
13
+ ...orderTools,
14
+ ...marketMakerTools,
15
+ ];
16
+ }
17
+ export { defineTool } from './types.js';
@@ -0,0 +1,2 @@
1
+ import { type AnyToolDefinition } from './types.js';
2
+ export declare const marketMakerTools: AnyToolDefinition[];
@@ -0,0 +1,38 @@
1
+ import { defineTool, READ_ONLY } from './types.js';
2
+ const PROFILE_DESCRIPTION = `
3
+ Fetch the **market-maker profile** for the API key's user. **Call this first** before running the market-maker workflow — it tells you the live economics the agent is operating under.
4
+
5
+ Returns the user's market-maker self view, including:
6
+ - **approvalStatus** (\`PENDING | APPROVED | SUSPENDED\`) — only \`APPROVED\` MMs get elevated limits and MM fees.
7
+ - **feeMode** (\`VOLUME | MANUAL\`) and **currentTier** — the active MM fee tier.
8
+ - **effectiveMakerFee** / **effectiveTakerFee** — the resolved signed fee rates at the current tier. Per-contract fee is \`price × (1 − price) × rate\`.
9
+ - **rateLimitProfile** and **orderRateLimit** — how many orders/min this account may place (MMs run on the \`market_maker\` preset).
10
+ - **exposure** / **exposureCap** — current net exposure and the (reporting-only) cap.
11
+ - **rebateRate** — fraction of maker notional paid back as a liquidity rebate.
12
+ - identity fields (\`entityName\`, \`contactEmail\`, \`website\`, \`jurisdiction\`, \`strategyNotes\`) and **recentRebates**.
13
+
14
+ This only works for accounts with the \`MARKET_MAKER\` role; a regular account gets a 403.
15
+
16
+ Requires scope: \`portfolio:read\`.
17
+
18
+ ### Example
19
+ No arguments:
20
+ \`\`\`json
21
+ {}
22
+ \`\`\`
23
+ `.trim();
24
+ export const marketMakerTools = [
25
+ defineTool({
26
+ name: 'get_market_maker_profile',
27
+ title: 'Get market maker profile',
28
+ description: PROFILE_DESCRIPTION,
29
+ inputSchema: {},
30
+ annotations: READ_ONLY,
31
+ handler: async (_args, ctx) => {
32
+ return ctx.client.request({
33
+ method: 'GET',
34
+ path: '/api/v1/market-maker/me',
35
+ });
36
+ },
37
+ }),
38
+ ];
@@ -0,0 +1,2 @@
1
+ import { type AnyToolDefinition } from './types.js';
2
+ export declare const marketTools: AnyToolDefinition[];
@@ -0,0 +1,120 @@
1
+ import { z } from 'zod';
2
+ import { defineTool, READ_ONLY } from './types.js';
3
+ const SEARCH_DESCRIPTION = `
4
+ List and search Unhedged prediction markets. Use this when the user asks about what markets exist, what they can bet on, what the odds are on a topic, or which markets are open right now.
5
+
6
+ Filter behaviour:
7
+ - **search**: free-text matched against the market question, tags, and category. Terms of four or more characters also match the description. Use this for topical queries like "election", "BTC price", "Lakers".
8
+ - **status**: filter by lifecycle. \`ACTIVE\` means open for trading, \`ENDED\` means closed but not yet resolved, \`RESOLVED\` means winners paid out, \`VOIDED\` means cancelled. Omit to include ACTIVE + ENDED. Request RESOLVED or VOIDED explicitly for historical markets.
9
+ - **category**: e.g. "crypto", "sports", "politics", "esports".
10
+ - **tag**: filter by one tag.
11
+ - **tags**: array of tags; markets must match ALL supplied tags.
12
+ - **orderBy**: one of \`createdAt\`, \`endTime\`, \`totalPool\`, \`question\`, \`status\`, \`betCount\`, \`payoutPool\`. Default \`createdAt\`.
13
+ - **orderDirection**: \`asc\` or \`desc\`. Default \`desc\`.
14
+ - **limit**: 1-100, default 20.
15
+ - **offset**: pagination, default 0.
16
+
17
+ Returns \`{ markets: MarketSummary[], total: number }\`. Each summary has \`id\`, \`question\`, \`status\`, \`outcomes\` (with labels), \`totalPool\`, \`endTime\`, plus category/tags metadata.
18
+
19
+ This endpoint is **public** — no API key scope required.
20
+
21
+ ### Example
22
+ Find the five most-recent active crypto markets:
23
+ \`\`\`json
24
+ { "status": "ACTIVE", "category": "crypto", "limit": 5 }
25
+ \`\`\`
26
+ `.trim();
27
+ const GET_MARKET_DESCRIPTION = `
28
+ Fetch the full record for a single market by ID. Call this after \`search_markets\` once you've narrowed down a candidate, or whenever you need the detailed outcome list / resolution rules / live event data.
29
+
30
+ Returns \`{ market }\` with all outcomes (labels + indexes), pool sizes per outcome, scheduled resolution time, auto-resolution config, optional live event data (sports), and an OG image URL for sharing.
31
+
32
+ Public — no scope required.
33
+
34
+ ### Example
35
+ \`\`\`json
36
+ { "id": "clxabc123..." }
37
+ \`\`\`
38
+ `.trim();
39
+ const GET_STATS_DESCRIPTION = `
40
+ Get market statistics: current implied probabilities per outcome, volume, bet count, payout pool, and time-weighted odds history pointers. Use this to compute edge, present odds to the user, or feed inputs to a hedged-position calculator.
41
+
42
+ Returns \`{ stats }\`. Stats shape mirrors the backend response and varies slightly by market type (PARIMUTUEL vs CLOB).
43
+
44
+ Public — no scope required.
45
+
46
+ ### Example
47
+ \`\`\`json
48
+ { "id": "clxabc123..." }
49
+ \`\`\`
50
+ `.trim();
51
+ const searchSchema = {
52
+ status: z.enum(['ACTIVE', 'ENDED', 'RESOLVED', 'VOIDED']).optional(),
53
+ category: z.string().optional(),
54
+ tag: z.string().max(50).optional(),
55
+ tags: z
56
+ .array(z.string().min(1).max(50))
57
+ .max(20)
58
+ .refine((tags) => tags.join(',').length <= 500, 'Combined tags must be at most 500 characters.')
59
+ .optional(),
60
+ search: z.string().min(2).max(200).optional(),
61
+ orderBy: z
62
+ .enum(['createdAt', 'endTime', 'totalPool', 'question', 'status', 'betCount', 'payoutPool'])
63
+ .optional(),
64
+ orderDirection: z.enum(['asc', 'desc']).optional(),
65
+ limit: z.number().int().min(1).max(100).optional(),
66
+ offset: z.number().int().min(0).optional(),
67
+ };
68
+ const idSchema = {
69
+ id: z.string().min(1).describe('Market ID (e.g. "clxabc123...")'),
70
+ };
71
+ export const marketTools = [
72
+ defineTool({
73
+ name: 'search_markets',
74
+ title: 'Search prediction markets',
75
+ description: SEARCH_DESCRIPTION,
76
+ inputSchema: searchSchema,
77
+ annotations: READ_ONLY,
78
+ handler: async (args, ctx) => {
79
+ const { tags, status, ...query } = args;
80
+ return ctx.client.request({
81
+ method: 'GET',
82
+ path: '/api/v1/markets',
83
+ // The REST API accepts one comma-separated `tags` value and applies
84
+ // hasEvery semantics. Keep the MCP input ergonomic while honoring that
85
+ // wire contract.
86
+ query: {
87
+ ...query,
88
+ status: status ?? 'ACTIVE,ENDED',
89
+ tags: tags && tags.length > 0 ? tags.join(',') : undefined,
90
+ },
91
+ });
92
+ },
93
+ }),
94
+ defineTool({
95
+ name: 'get_market',
96
+ title: 'Get market by ID',
97
+ description: GET_MARKET_DESCRIPTION,
98
+ inputSchema: idSchema,
99
+ annotations: READ_ONLY,
100
+ handler: async (args, ctx) => {
101
+ return ctx.client.request({
102
+ method: 'GET',
103
+ path: `/api/v1/markets/${encodeURIComponent(args.id)}`,
104
+ });
105
+ },
106
+ }),
107
+ defineTool({
108
+ name: 'get_market_stats',
109
+ title: 'Get market statistics',
110
+ description: GET_STATS_DESCRIPTION,
111
+ inputSchema: idSchema,
112
+ annotations: READ_ONLY,
113
+ handler: async (args, ctx) => {
114
+ return ctx.client.request({
115
+ method: 'GET',
116
+ path: `/api/v1/markets/${encodeURIComponent(args.id)}/stats`,
117
+ });
118
+ },
119
+ }),
120
+ ];
@@ -0,0 +1,2 @@
1
+ import { type AnyToolDefinition } from './types.js';
2
+ export declare const orderTools: AnyToolDefinition[];