@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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +48 -0
- package/dist/client/errors.d.ts +15 -0
- package/dist/client/errors.js +40 -0
- package/dist/client/http.d.ts +21 -0
- package/dist/client/http.js +132 -0
- package/dist/client/types.d.ts +140 -0
- package/dist/client/types.js +6 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +27 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/init/index.d.ts +10 -0
- package/dist/init/index.js +137 -0
- package/dist/init/snippet.d.ts +35 -0
- package/dist/init/snippet.js +72 -0
- package/dist/prompts/frontmatter.d.ts +28 -0
- package/dist/prompts/frontmatter.js +113 -0
- package/dist/prompts/index.d.ts +10 -0
- package/dist/prompts/index.js +50 -0
- package/dist/prompts/loader.d.ts +13 -0
- package/dist/prompts/loader.js +36 -0
- package/dist/safety/index.d.ts +1 -0
- package/dist/safety/index.js +1 -0
- package/dist/safety/spend-cap.d.ts +33 -0
- package/dist/safety/spend-cap.js +64 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +75 -0
- package/dist/tools/balance.d.ts +2 -0
- package/dist/tools/balance.js +175 -0
- package/dist/tools/bets.d.ts +2 -0
- package/dist/tools/bets.js +109 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/market-maker.d.ts +2 -0
- package/dist/tools/market-maker.js +38 -0
- package/dist/tools/markets.d.ts +2 -0
- package/dist/tools/markets.js +120 -0
- package/dist/tools/orders.d.ts +2 -0
- package/dist/tools/orders.js +256 -0
- package/dist/tools/portfolio.d.ts +2 -0
- package/dist/tools/portfolio.js +34 -0
- package/dist/tools/types.d.ts +44 -0
- package/dist/tools/types.js +10 -0
- package/package.json +54 -0
- package/skills/cash-out.md +51 -0
- package/skills/market-creator-assistant.md +45 -0
- package/skills/market-maker-workflow.md +58 -0
- package/skills/market-research.md +42 -0
- package/skills/place-hedged-position.md +51 -0
- package/skills/portfolio-review.md +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alpha DNA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @unhedged/mcp
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for the [Unhedged](https://unhedged.gg) prediction-markets platform. Lets AI agents — Claude Desktop, Cursor, Claude CLI, Codex CLI, or other clients that support local stdio servers — interact with Unhedged on behalf of an end user via a scoped API key.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Exposes 12 MCP **tools** over the Unhedged v1 REST API: search markets, read balances and portfolio, place bets and orders, cancel orders, request withdrawals, and read a market-maker profile.
|
|
8
|
+
- Ships 6 MCP **prompts** ("skills") — opinionated markdown workflows the agent can execute end-to-end (e.g. `place-hedged-position`, `cash-out`, `portfolio-review`).
|
|
9
|
+
- Auto-generates an `Idempotency-Key` for every state-changing call. Each new call uses a fresh key, so ambiguous failures are not retried automatically. Check the account's transaction history before repeating a mutation; open orders and balances cannot rule out a filled order or completed withdrawal. Ask the user if the outcome remains uncertain.
|
|
10
|
+
- Declares MCP tool annotations (`readOnlyHint`, `destructiveHint`); bets, orders, cancellations, and withdrawals are marked destructive. Confirmation behavior depends on the client.
|
|
11
|
+
- Optional control: `UNHEDGED_MAX_SPEND_PER_SESSION` for a cumulative bet-stake and buy-order notional cap. Fees and withdrawals are excluded from this cap.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @unhedged/mcp init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The init wizard asks which client to wire up (Claude Desktop, Cursor, Claude CLI, or Codex CLI), prompts for your Unhedged API key, and writes a config file or prints a client command. The CLI commands include the key; run them off camera and avoid sharing terminal history.
|
|
20
|
+
|
|
21
|
+
Mint an API key at <https://unhedged.gg/account/api-keys>. See the [API key minting walkthrough](#api-key-minting) below for recommended scope sets.
|
|
22
|
+
|
|
23
|
+
## Environment variables
|
|
24
|
+
|
|
25
|
+
| Variable | Required | Description |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `UNHEDGED_API_KEY` | yes | Bearer token, format `ak_...`. Scopes minted into the key determine which tools work. |
|
|
28
|
+
| `UNHEDGED_MAX_SPEND_PER_SESSION` | no | Cap (in USD) on cumulative bet stakes and buy-order notional per server lifetime. Excludes fees and withdrawals. |
|
|
29
|
+
|
|
30
|
+
## API key minting
|
|
31
|
+
|
|
32
|
+
Recommended scope sets per use case:
|
|
33
|
+
|
|
34
|
+
| Use case | Scopes |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Read-only research | `market:read`, `portfolio:read`, `bet:read`, `order:read`, `balance:read` |
|
|
37
|
+
| Full trading | above + `bet:place`, `order:place`, `order:cancel` |
|
|
38
|
+
| Full trading + withdrawals | above + `balance:withdraw` |
|
|
39
|
+
| Market maker (CLOB quoting) | `order:place`, `order:cancel`, `order:read`, `market:read`, `balance:read`, `portfolio:read` |
|
|
40
|
+
|
|
41
|
+
> ⚠️ **Withdrawals are off by default.** Don't include `balance:withdraw` unless you want the agent to be able to move funds off the platform.
|
|
42
|
+
|
|
43
|
+
### Market makers
|
|
44
|
+
|
|
45
|
+
Accounts with the `MARKET_MAKER` role get a more favorable fee tier and **elevated order rate limits** (the `market_maker` rate-limit profile: up to 1,000,000 orders/min total and per market, versus the defaults of 120/min total and 60/min per market), so high-frequency quoting via the MCP isn't throttled. The role is granted by an Unhedged admin — there is no self-service application yet. Once granted, call `get_market_maker_profile` to read the live tier, effective maker/taker fees, limits, and rebate terms, then run the `market-maker-workflow` skill. A market-maker key needs `order:place`, `order:cancel`, `order:read`, `market:read`, `balance:read`, and `portfolio:read`.
|
|
46
|
+
|
|
47
|
+
## Tools
|
|
48
|
+
|
|
49
|
+
Market searches default to ACTIVE and ENDED. Pass an explicit status to browse resolved or voided markets.
|
|
50
|
+
|
|
51
|
+
Tool descriptions are available in your MCP client. Quick reference:
|
|
52
|
+
|
|
53
|
+
| Tool | Scope | Idempotency key attached |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `search_markets` | (public) | n/a |
|
|
56
|
+
| `get_market` | (public) | n/a |
|
|
57
|
+
| `get_market_stats` | (public) | n/a |
|
|
58
|
+
| `get_balance` | `balance:read` | n/a |
|
|
59
|
+
| `get_portfolio` | `portfolio:read` | n/a |
|
|
60
|
+
| `list_bets` | `bet:read` | n/a |
|
|
61
|
+
| `place_bet` | `bet:place` | yes |
|
|
62
|
+
| `list_orders` | `order:read` | n/a |
|
|
63
|
+
| `place_order` | `order:place` | yes |
|
|
64
|
+
| `cancel_order` | `order:cancel` | yes |
|
|
65
|
+
| `request_withdrawal` | `balance:withdraw` | yes |
|
|
66
|
+
| `get_market_maker_profile` | `portfolio:read` | n/a |
|
|
67
|
+
|
|
68
|
+
## Skills
|
|
69
|
+
|
|
70
|
+
Markdown workflows under `skills/`, surfaced to MCP clients as slash-commands / prompts:
|
|
71
|
+
|
|
72
|
+
- `market-research` — find markets matching a topic, summarize odds and resolution rules.
|
|
73
|
+
- `place-hedged-position` — calculate and place a hedged position across outcomes.
|
|
74
|
+
- `portfolio-review` — read-only summary of holdings, P&L, and risk.
|
|
75
|
+
- `market-maker-workflow` — quote both sides of an order book. Requires the `MARKET_MAKER` role; call `get_market_maker_profile` first to read the live fee tier and limits.
|
|
76
|
+
- `cash-out` — close open orders, accept market prices, prepare for withdrawal.
|
|
77
|
+
- `market-creator-assistant` — draft a market specification and guide submission through the web UI. Requires the `MARKET_CREATOR` role.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startStdioServer } from './server.js';
|
|
3
|
+
const HELP = `
|
|
4
|
+
@unhedged/mcp — Model Context Protocol server for the Unhedged platform.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
npx @unhedged/mcp Start the MCP server over stdio (what MCP clients invoke).
|
|
8
|
+
npx @unhedged/mcp init Interactive setup for Claude Desktop, Cursor,
|
|
9
|
+
Claude CLI, or Codex CLI.
|
|
10
|
+
npx @unhedged/mcp --help Show this help.
|
|
11
|
+
|
|
12
|
+
Environment:
|
|
13
|
+
UNHEDGED_API_KEY (required) Bearer token in format "ak_...".
|
|
14
|
+
UNHEDGED_MAX_SPEND_PER_SESSION (optional) Cap on bet stakes and buy-order notional per session.
|
|
15
|
+
Excludes fees and withdrawals; resets on restart.
|
|
16
|
+
|
|
17
|
+
Docs: https://unhedged.gg/mcp
|
|
18
|
+
`.trim();
|
|
19
|
+
async function main() {
|
|
20
|
+
const subcommand = process.argv[2];
|
|
21
|
+
if (subcommand === '--help' || subcommand === '-h') {
|
|
22
|
+
process.stdout.write(HELP + '\n');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (subcommand === 'init') {
|
|
26
|
+
const { runInit } = await import('./init/index.js');
|
|
27
|
+
await runInit();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (subcommand) {
|
|
31
|
+
const kind = subcommand.startsWith('-') ? 'flag' : 'subcommand';
|
|
32
|
+
process.stderr.write(`Unknown ${kind}: "${subcommand}".\n\n${HELP}\n`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
// No subcommand → run as MCP server over stdio.
|
|
36
|
+
try {
|
|
37
|
+
await startStdioServer();
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
41
|
+
process.stderr.write(`[unhedged-mcp] startup failed: ${msg}\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
main().catch((err) => {
|
|
46
|
+
process.stderr.write(`[unhedged-mcp] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class UnhedgedApiError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly code: string | undefined;
|
|
4
|
+
readonly missingScope: string | undefined;
|
|
5
|
+
readonly retryable: boolean;
|
|
6
|
+
readonly responseBody: unknown;
|
|
7
|
+
constructor(args: {
|
|
8
|
+
status: number;
|
|
9
|
+
message: string;
|
|
10
|
+
code?: string;
|
|
11
|
+
missingScope?: string;
|
|
12
|
+
responseBody?: unknown;
|
|
13
|
+
});
|
|
14
|
+
toAgentMessage(stateChanging?: boolean): string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class UnhedgedApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
missingScope;
|
|
5
|
+
retryable;
|
|
6
|
+
responseBody;
|
|
7
|
+
constructor(args) {
|
|
8
|
+
super(args.message);
|
|
9
|
+
this.name = 'UnhedgedApiError';
|
|
10
|
+
this.status = args.status;
|
|
11
|
+
this.code = args.code;
|
|
12
|
+
this.missingScope = args.missingScope;
|
|
13
|
+
this.responseBody = args.responseBody;
|
|
14
|
+
// Status 0 = network error / timeout: the response never arrived, so the
|
|
15
|
+
// outcome is ambiguous. `retryable` describes the transport condition; it
|
|
16
|
+
// does not mean a financial mutation should be retried without checking
|
|
17
|
+
// whether the first request landed.
|
|
18
|
+
this.retryable = args.status >= 500 || args.status === 429 || args.status === 0;
|
|
19
|
+
}
|
|
20
|
+
toAgentMessage(stateChanging = false) {
|
|
21
|
+
if (this.missingScope) {
|
|
22
|
+
return (`Unhedged API rejected the call (${this.status}): the API key is missing scope ` +
|
|
23
|
+
`"${this.missingScope}". Mint a new key including this scope at ` +
|
|
24
|
+
`https://unhedged.gg/account/api-keys.`);
|
|
25
|
+
}
|
|
26
|
+
if (this.status === 401) {
|
|
27
|
+
return `Unhedged API rejected the API key (401). Verify UNHEDGED_API_KEY is set and the key is still active.`;
|
|
28
|
+
}
|
|
29
|
+
if (this.retryable) {
|
|
30
|
+
if (!stateChanging) {
|
|
31
|
+
return `Unhedged API returned ${this.status} (${this.message}). This may be transient; retry the read request.`;
|
|
32
|
+
}
|
|
33
|
+
return (`Unhedged API returned ${this.status} (${this.message}). This may be transient. ` +
|
|
34
|
+
`Do not repeat a state-changing call until its outcome is confirmed. ` +
|
|
35
|
+
`Open orders and balances alone cannot rule out a filled order or completed withdrawal. ` +
|
|
36
|
+
`Check the account's transaction history on Unhedged; if the result is still uncertain, stop and ask the user.`);
|
|
37
|
+
}
|
|
38
|
+
return `Unhedged API returned ${this.status}: ${this.message}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { UnhedgedMcpConfig } from '../config.js';
|
|
2
|
+
export interface RequestOptions {
|
|
3
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
4
|
+
path: string;
|
|
5
|
+
/**
|
|
6
|
+
* Query parameters. Values that are `undefined`/`null` are skipped. Arrays
|
|
7
|
+
* are emitted as repeated keys. Anything else is coerced via String().
|
|
8
|
+
*/
|
|
9
|
+
query?: Record<string, unknown>;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
/** Override or disable auto-generated Idempotency-Key. */
|
|
12
|
+
idempotencyKey?: string | false;
|
|
13
|
+
}
|
|
14
|
+
export declare class UnhedgedHttpClient {
|
|
15
|
+
private readonly apiKey;
|
|
16
|
+
private readonly baseUrl;
|
|
17
|
+
constructor(config: UnhedgedMcpConfig);
|
|
18
|
+
request<T>(options: RequestOptions): Promise<T>;
|
|
19
|
+
private attempt;
|
|
20
|
+
private buildUrl;
|
|
21
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { UnhedgedApiError } from './errors.js';
|
|
3
|
+
const STATE_CHANGING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
5
|
+
export class UnhedgedHttpClient {
|
|
6
|
+
apiKey;
|
|
7
|
+
baseUrl;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.apiKey = config.apiKey;
|
|
10
|
+
this.baseUrl = config.baseUrl;
|
|
11
|
+
}
|
|
12
|
+
async request(options) {
|
|
13
|
+
const method = options.method ?? 'GET';
|
|
14
|
+
const url = this.buildUrl(options.path, options.query);
|
|
15
|
+
const headers = {
|
|
16
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
17
|
+
Accept: 'application/json',
|
|
18
|
+
};
|
|
19
|
+
let body;
|
|
20
|
+
if (options.body !== undefined) {
|
|
21
|
+
headers['Content-Type'] = 'application/json';
|
|
22
|
+
body = JSON.stringify(options.body);
|
|
23
|
+
}
|
|
24
|
+
const idempotencyKey = STATE_CHANGING_METHODS.has(method) && options.idempotencyKey !== false
|
|
25
|
+
? options.idempotencyKey ?? randomUUID()
|
|
26
|
+
: undefined;
|
|
27
|
+
if (idempotencyKey !== undefined) {
|
|
28
|
+
headers['Idempotency-Key'] = idempotencyKey;
|
|
29
|
+
}
|
|
30
|
+
// Do not automatically retry mutations. An Idempotency-Key lets the
|
|
31
|
+
// backend replay a completed request, but it does not prove that an
|
|
32
|
+
// in-flight request has stopped after the client times out. Retrying here
|
|
33
|
+
// could race the original operation and duplicate a financial mutation.
|
|
34
|
+
// Surface the ambiguous result so the agent can verify state first.
|
|
35
|
+
return this.attempt(method, url, headers, body);
|
|
36
|
+
}
|
|
37
|
+
async attempt(method, url, headers, body) {
|
|
38
|
+
let response;
|
|
39
|
+
try {
|
|
40
|
+
response = await fetch(url, {
|
|
41
|
+
method,
|
|
42
|
+
headers,
|
|
43
|
+
body,
|
|
44
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const timedOut = err instanceof Error && err.name === 'TimeoutError';
|
|
49
|
+
const msg = timedOut
|
|
50
|
+
? `request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`
|
|
51
|
+
: err instanceof Error
|
|
52
|
+
? err.message
|
|
53
|
+
: String(err);
|
|
54
|
+
throw new UnhedgedApiError({
|
|
55
|
+
status: 0,
|
|
56
|
+
message: `Network error calling Unhedged API: ${msg}`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
await throwApiError(response);
|
|
61
|
+
}
|
|
62
|
+
if (response.status === 204) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
if (!text)
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(text);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new UnhedgedApiError({
|
|
73
|
+
status: response.status,
|
|
74
|
+
message: `Unhedged API returned non-JSON body (status ${response.status}).`,
|
|
75
|
+
responseBody: text,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
buildUrl(path, query) {
|
|
80
|
+
// Concatenate rather than `new URL(path, base)` so a base URL with a path
|
|
81
|
+
// component (e.g. a reverse proxy at https://host/unhedged) is preserved.
|
|
82
|
+
const url = new URL(`${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`);
|
|
83
|
+
if (query) {
|
|
84
|
+
for (const [key, value] of Object.entries(query)) {
|
|
85
|
+
if (value === undefined || value === null)
|
|
86
|
+
continue;
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (const v of value) {
|
|
89
|
+
if (v !== undefined && v !== null)
|
|
90
|
+
url.searchParams.append(key, String(v));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
url.searchParams.append(key, String(value));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return url.toString();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function throwApiError(response) {
|
|
102
|
+
let parsed;
|
|
103
|
+
let rawText = '';
|
|
104
|
+
try {
|
|
105
|
+
rawText = await response.text();
|
|
106
|
+
if (rawText)
|
|
107
|
+
parsed = JSON.parse(rawText);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// body wasn't JSON
|
|
111
|
+
}
|
|
112
|
+
const body = (parsed ?? {});
|
|
113
|
+
const message = typeof body.message === 'string'
|
|
114
|
+
? body.message
|
|
115
|
+
: typeof body.error === 'string'
|
|
116
|
+
? body.error
|
|
117
|
+
: rawText || `HTTP ${response.status}`;
|
|
118
|
+
const code = typeof body.code === 'string' ? body.code : undefined;
|
|
119
|
+
let missingScope;
|
|
120
|
+
if (response.status === 403 && typeof body.message === 'string') {
|
|
121
|
+
const m = body.message.match(/missing required scope:\s*([^\s,]+)/i);
|
|
122
|
+
if (m)
|
|
123
|
+
missingScope = m[1];
|
|
124
|
+
}
|
|
125
|
+
throw new UnhedgedApiError({
|
|
126
|
+
status: response.status,
|
|
127
|
+
message,
|
|
128
|
+
code,
|
|
129
|
+
missingScope,
|
|
130
|
+
responseBody: parsed ?? rawText,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response shapes mirrored from the Unhedged backend route handlers. Kept loose
|
|
3
|
+
* (most fields typed but extras allowed) so additive backend changes don't break
|
|
4
|
+
* the MCP build. If a backend response changes shape, update here.
|
|
5
|
+
*/
|
|
6
|
+
export type MarketStatus = 'ACTIVE' | 'ENDED' | 'RESOLVED' | 'VOIDED';
|
|
7
|
+
export type BetStatus = 'PENDING' | 'CONFIRMED' | 'FAILED' | 'REFUNDED' | 'WON' | 'LOST';
|
|
8
|
+
export interface MarketSummary {
|
|
9
|
+
id: string;
|
|
10
|
+
question: string;
|
|
11
|
+
status: MarketStatus;
|
|
12
|
+
category?: string;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
outcomes?: Array<{
|
|
15
|
+
index?: number;
|
|
16
|
+
label: string;
|
|
17
|
+
}>;
|
|
18
|
+
totalPool?: string | number;
|
|
19
|
+
endTime?: string;
|
|
20
|
+
scheduledResolutionTime?: string | null;
|
|
21
|
+
createdAt?: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface MarketDetail extends MarketSummary {
|
|
25
|
+
outcomeStats?: unknown;
|
|
26
|
+
autoResolutionConfig?: unknown;
|
|
27
|
+
liveEventData?: unknown;
|
|
28
|
+
ogImageUrl?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ListMarketsResponse {
|
|
31
|
+
markets: MarketSummary[];
|
|
32
|
+
total?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface GetMarketResponse {
|
|
35
|
+
market: MarketDetail;
|
|
36
|
+
}
|
|
37
|
+
export interface MarketStats {
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
export interface GetMarketStatsResponse {
|
|
41
|
+
stats: MarketStats;
|
|
42
|
+
}
|
|
43
|
+
export interface BalanceResponse {
|
|
44
|
+
balance: {
|
|
45
|
+
available: string;
|
|
46
|
+
lockedWithdraws: string;
|
|
47
|
+
lockedBets: string;
|
|
48
|
+
lockedEngine: string;
|
|
49
|
+
total: string;
|
|
50
|
+
totalDeposited?: string;
|
|
51
|
+
totalWithdrawn?: string;
|
|
52
|
+
};
|
|
53
|
+
withdrawalFee: string;
|
|
54
|
+
withdrawalMinAmount: string;
|
|
55
|
+
bettingFee?: string;
|
|
56
|
+
ccPriceUsd: number | null;
|
|
57
|
+
}
|
|
58
|
+
export interface PortfolioResponse {
|
|
59
|
+
portfolio: {
|
|
60
|
+
summary: {
|
|
61
|
+
totalBets: number;
|
|
62
|
+
totalWagered: string;
|
|
63
|
+
totalWins: number;
|
|
64
|
+
totalLosses: number;
|
|
65
|
+
pendingBets: number;
|
|
66
|
+
pendingOrderBookPositions?: number;
|
|
67
|
+
pendingOrderBookCostBasis?: string;
|
|
68
|
+
totalPayouts: string;
|
|
69
|
+
totalProfit: string;
|
|
70
|
+
winRate: number;
|
|
71
|
+
roi: number;
|
|
72
|
+
};
|
|
73
|
+
positions: unknown[];
|
|
74
|
+
/** The complete active/ended row count represented by the paged positions endpoint. */
|
|
75
|
+
positionsTotal: number;
|
|
76
|
+
recentPayouts: unknown[];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export interface Bet {
|
|
80
|
+
id: string;
|
|
81
|
+
marketId: string;
|
|
82
|
+
outcomeIndex: number;
|
|
83
|
+
amount: string;
|
|
84
|
+
status: BetStatus;
|
|
85
|
+
timeWeight?: string;
|
|
86
|
+
createdAt?: string;
|
|
87
|
+
[key: string]: unknown;
|
|
88
|
+
}
|
|
89
|
+
export interface ListBetsResponse {
|
|
90
|
+
bets: Bet[];
|
|
91
|
+
total: number;
|
|
92
|
+
}
|
|
93
|
+
export interface PlaceBetResponse {
|
|
94
|
+
bet: Bet;
|
|
95
|
+
balanceAfter: string;
|
|
96
|
+
fee: string;
|
|
97
|
+
}
|
|
98
|
+
export interface MEOrder {
|
|
99
|
+
id?: string | number;
|
|
100
|
+
marketId: string;
|
|
101
|
+
outcome: string;
|
|
102
|
+
side: 'bid' | 'ask';
|
|
103
|
+
type: 'Limit' | 'Market' | 'StopLoss' | 'TakeProfit';
|
|
104
|
+
price?: string;
|
|
105
|
+
triggerPrice?: string;
|
|
106
|
+
quantity?: number;
|
|
107
|
+
quoteQuantity?: string;
|
|
108
|
+
status?: string;
|
|
109
|
+
[key: string]: unknown;
|
|
110
|
+
}
|
|
111
|
+
export interface ListOrdersResponse {
|
|
112
|
+
orders: MEOrder[];
|
|
113
|
+
nextCursor?: string | null;
|
|
114
|
+
total?: number;
|
|
115
|
+
markets?: unknown[];
|
|
116
|
+
}
|
|
117
|
+
export interface PlaceOrderResponse {
|
|
118
|
+
order: MEOrder;
|
|
119
|
+
}
|
|
120
|
+
export interface CancelOrderResponse {
|
|
121
|
+
result: unknown;
|
|
122
|
+
}
|
|
123
|
+
export interface EthereumWithdrawQuoteResponse {
|
|
124
|
+
quote: {
|
|
125
|
+
quoteId: string;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export interface WithdrawalResponse {
|
|
129
|
+
withdrawal: {
|
|
130
|
+
id: string;
|
|
131
|
+
amount: string;
|
|
132
|
+
fee: string;
|
|
133
|
+
totalDeducted: string;
|
|
134
|
+
status: string;
|
|
135
|
+
createdAt: string;
|
|
136
|
+
network?: 'CANTON' | 'SOLANA' | 'TRON' | 'ETHEREUM';
|
|
137
|
+
destinationAddress?: string;
|
|
138
|
+
};
|
|
139
|
+
message: string;
|
|
140
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const DEFAULT_BASE_URL = 'https://api.unhedged.gg';
|
|
2
|
+
export function loadConfig(env = process.env) {
|
|
3
|
+
const apiKey = env.UNHEDGED_API_KEY?.trim();
|
|
4
|
+
if (!apiKey) {
|
|
5
|
+
throw new Error('UNHEDGED_API_KEY environment variable is required. ' +
|
|
6
|
+
'Mint a key at https://unhedged.gg/account/api-keys and pass it via the MCP client config.');
|
|
7
|
+
}
|
|
8
|
+
if (!apiKey.startsWith('ak_')) {
|
|
9
|
+
throw new Error(`UNHEDGED_API_KEY has unexpected format. Expected "ak_..." but got "${apiKey.slice(0, 6)}...".`);
|
|
10
|
+
}
|
|
11
|
+
const userBaseUrl = env.UNHEDGED_BASE_URL?.trim();
|
|
12
|
+
const baseUrl = (userBaseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
13
|
+
if (['true', '1', 'yes', 'on'].includes(env.UNHEDGED_DRY_RUN?.trim().toLowerCase() ?? '')) {
|
|
14
|
+
throw new Error('Preview mode is no longer supported. Remove UNHEDGED_DRY_RUN only when you intend to submit real actions.');
|
|
15
|
+
}
|
|
16
|
+
const maxSpendPerSession = parseSpendCap(env.UNHEDGED_MAX_SPEND_PER_SESSION);
|
|
17
|
+
return { apiKey, baseUrl, maxSpendPerSession };
|
|
18
|
+
}
|
|
19
|
+
function parseSpendCap(value) {
|
|
20
|
+
if (!value || !value.trim())
|
|
21
|
+
return null;
|
|
22
|
+
const n = Number(value);
|
|
23
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
24
|
+
throw new Error(`UNHEDGED_MAX_SPEND_PER_SESSION must be a positive number; got "${value}".`);
|
|
25
|
+
}
|
|
26
|
+
return n;
|
|
27
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createServer, startStdioServer } from './server.js';
|
|
2
|
+
export { UnhedgedHttpClient } from './client/http.js';
|
|
3
|
+
export { UnhedgedApiError } from './client/errors.js';
|
|
4
|
+
export { loadConfig } from './config.js';
|
|
5
|
+
export type { UnhedgedMcpConfig } from './config.js';
|
|
6
|
+
export type { AnyToolDefinition, ToolDefinition, ToolContext } from './tools/types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type InitTarget = 'claude-desktop' | 'cursor' | 'claude-cli' | 'codex-cli' | 'print';
|
|
2
|
+
export declare function runInit(): Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Read an existing JSON config (if present), deep-merge at the requested path,
|
|
5
|
+
* and write it back. Preserves any other top-level keys the user has set.
|
|
6
|
+
* `parentKeys` must descend to the object that owns the server entries —
|
|
7
|
+
* merging a block that itself contains `mcpServers` at the top level would
|
|
8
|
+
* clobber every other server the user has configured.
|
|
9
|
+
*/
|
|
10
|
+
export declare function mergeJsonConfig(path: string, parentKeys: string[], block: object): void;
|