polymarket-dvm-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/index.js +146 -0
- package/dist/ndk.js +94 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Polymarket DVM MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that interfaces with the Polymarket Data Verification Mechanism (DVM) via Nostr. This server allows AI agents to search for prediction markets and obtain detailed AI summaries.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Search Markets**: Search for prediction markets on Polymarket using keywords.
|
|
8
|
+
- **AI Summary**: Get deep market analysis and AI-generated summaries for specific markets using their numerical IDs.
|
|
9
|
+
- **Payment Handling**: Seamlessly handles Nostr DVM feedback (Kind 7000), providing lightning invoices when payment is required for searches or summaries.
|
|
10
|
+
- **NDK Integration**: Uses the Nostr Dev Kit (NDK) for robust Nostr communication.
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
- Node.js (v18 or higher)
|
|
15
|
+
- npm or yarn
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
1. Clone the repository and navigate to the project directory.
|
|
20
|
+
2. Install dependencies:
|
|
21
|
+
```bash
|
|
22
|
+
npm install
|
|
23
|
+
```
|
|
24
|
+
3. Build the project:
|
|
25
|
+
```bash
|
|
26
|
+
npm run build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
The server can be configured via environment variables:
|
|
32
|
+
|
|
33
|
+
- `NOSTR_NSEC`: (Optional) Your Nostr private key in `nsec` format. If not provided, a random identity will be generated for each session.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Running the Server
|
|
38
|
+
|
|
39
|
+
Start the server using `ts-node`:
|
|
40
|
+
```bash
|
|
41
|
+
npm start
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or run the compiled JavaScript (after building):
|
|
45
|
+
```bash
|
|
46
|
+
npm run serve
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### MCP Tools
|
|
50
|
+
|
|
51
|
+
The server exposes the following tools to MCP clients (like Claude Desktop):
|
|
52
|
+
|
|
53
|
+
1. **`search_polymarket`**
|
|
54
|
+
- **Description**: Search for prediction markets. Returns markets with their questions and IDs.
|
|
55
|
+
- **Arguments**: `query` (string)
|
|
56
|
+
- **Note**: May return a lightning invoice during high utilization.
|
|
57
|
+
|
|
58
|
+
2. **`get_market`**
|
|
59
|
+
- **Description**: Get a detailed AI summary for a specific market.
|
|
60
|
+
- **Arguments**: `marketId` (string, the numerical ID from search results)
|
|
61
|
+
- **Note**: Usually requires payment via lightning invoice.
|
|
62
|
+
|
|
63
|
+
## Deployment with Docker
|
|
64
|
+
|
|
65
|
+
You can run the MCP server using Docker:
|
|
66
|
+
|
|
67
|
+
1. Build the image:
|
|
68
|
+
```bash
|
|
69
|
+
docker build -t polymarket-dvm-mcp .
|
|
70
|
+
```
|
|
71
|
+
2. Run the container:
|
|
72
|
+
```bash
|
|
73
|
+
docker run -i -e NOSTR_NSEC=your_nsec_here polymarket-dvm-mcp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Security Note
|
|
77
|
+
|
|
78
|
+
Your `nsec` is sensitive information. Never commit it to version control. Use environment variables to pass it safely to the application.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { NostrClient } from "./ndk.js";
|
|
5
|
+
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|
6
|
+
const server = new Server({
|
|
7
|
+
name: "polymarket-dvm-mcp",
|
|
8
|
+
version: "0.1.0",
|
|
9
|
+
}, {
|
|
10
|
+
capabilities: {
|
|
11
|
+
tools: {},
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
// Helper for structured logging to stderr (to avoid interfering with MCP stdio)
|
|
15
|
+
const log = (level, message, data) => {
|
|
16
|
+
const timestamp = new Date().toISOString();
|
|
17
|
+
console.error(JSON.stringify({ timestamp, level, message, data }));
|
|
18
|
+
};
|
|
19
|
+
const nsec = process.env.NOSTR_NSEC;
|
|
20
|
+
let signer;
|
|
21
|
+
if (nsec) {
|
|
22
|
+
try {
|
|
23
|
+
if (!nsec.startsWith('nsec1')) {
|
|
24
|
+
throw new Error("Invalid nsec format. Must start with 'nsec1'");
|
|
25
|
+
}
|
|
26
|
+
signer = new NDKPrivateKeySigner(nsec);
|
|
27
|
+
log("INFO", "Nostr identity loaded from NOSTR_NSEC");
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
log("ERROR", `Failed to initialize Nostr signer: ${e.message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
log("INFO", "No NOSTR_NSEC provided, generating random identity");
|
|
36
|
+
signer = NDKPrivateKeySigner.generate();
|
|
37
|
+
}
|
|
38
|
+
const nostrClient = new NostrClient(signer);
|
|
39
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
40
|
+
return {
|
|
41
|
+
tools: [
|
|
42
|
+
{
|
|
43
|
+
name: "search_polymarket",
|
|
44
|
+
description: "Search for prediction markets on Polymarket. Returns a list of markets with their questions, descriptions, and numerical 'id' values. Use these numerical IDs to call 'get_market' for detailed analysis. Note: May return a JSON object with a lightning invoice if payment is required due to high utilization.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
query: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "The search term or question to search for (e.g., 'Will Bitcoin hit $100k?').",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ["query"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "get_market",
|
|
58
|
+
description: "Get a detailed AI summary and deep market analysis for a specific Polymarket event. Requires a numerical market 'id' obtained from 'search_polymarket'. If the DVM requires payment for the AI summary, this tool will return a lightning invoice to be paid before the summary is provided.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
marketId: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The numerical market ID from search results (e.g., '956590').",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ["marketId"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
74
|
+
const { name, arguments: args } = request.params;
|
|
75
|
+
try {
|
|
76
|
+
log("DEBUG", `Connecting to Nostr relays for tool: ${name}`);
|
|
77
|
+
await nostrClient.connect();
|
|
78
|
+
if (name === "search_polymarket") {
|
|
79
|
+
const query = String(args?.query);
|
|
80
|
+
log("INFO", "Performing Polymarket search", { query });
|
|
81
|
+
const result = await nostrClient.requestSearch(query);
|
|
82
|
+
if ("status" in result && result.status === "payment-required") {
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `Payment required to perform this search due to high utilization.\n\nAmount: ${result.amount}\nInvoice: ${result.invoice}\n\nPlease pay the invoice and call this tool again once paid.`,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: JSON.stringify(result, null, 2),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (name === "get_market") {
|
|
102
|
+
const marketId = String(args?.marketId);
|
|
103
|
+
log("INFO", "Fetching market summary", { marketId });
|
|
104
|
+
const result = await nostrClient.requestSummary(marketId);
|
|
105
|
+
if ("status" in result && result.status === "payment-required") {
|
|
106
|
+
return {
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "text",
|
|
110
|
+
text: `Payment required to access this summary.\n\nAmount: ${result.amount}\nInvoice: ${result.invoice}\n\nPlease pay the invoice and call this tool again once paid.`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: JSON.stringify(result, null, 2),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
type: "text",
|
|
131
|
+
text: `Error: ${error.message}`,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
async function main() {
|
|
139
|
+
const transport = new StdioServerTransport();
|
|
140
|
+
await server.connect(transport);
|
|
141
|
+
console.error("Polymarket DVM MCP server running on stdio");
|
|
142
|
+
}
|
|
143
|
+
main().catch((error) => {
|
|
144
|
+
console.error("Server error:", error);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
});
|
package/dist/ndk.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import NDK, { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
// Required for NDK in Node.js
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
global.WebSocket = WebSocket;
|
|
6
|
+
export const DVM_PUBKEY = '813c654f1b7a4996c8f4769079288a0eace4380dd55e71949116100759b9dddc';
|
|
7
|
+
export const RELAYS = ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net'];
|
|
8
|
+
export const KIND_SEARCH_REQUEST = 5005;
|
|
9
|
+
export const KIND_SEARCH_RESULT = 6005;
|
|
10
|
+
export const KIND_SUMMARY_REQUEST = 5001;
|
|
11
|
+
export const KIND_SUMMARY_RESULT = 6001;
|
|
12
|
+
export const KIND_FEEDBACK = 7000;
|
|
13
|
+
export class NostrClient {
|
|
14
|
+
ndk;
|
|
15
|
+
constructor(signer) {
|
|
16
|
+
this.ndk = new NDK({
|
|
17
|
+
explicitRelayUrls: RELAYS,
|
|
18
|
+
signer: signer || NDKPrivateKeySigner.generate(),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async connect() {
|
|
22
|
+
await this.ndk.connect();
|
|
23
|
+
console.error('Connected to relays');
|
|
24
|
+
}
|
|
25
|
+
async requestSearch(query) {
|
|
26
|
+
const event = new NDKEvent(this.ndk);
|
|
27
|
+
event.kind = KIND_SEARCH_REQUEST;
|
|
28
|
+
event.content = query;
|
|
29
|
+
event.tags = [
|
|
30
|
+
['p', DVM_PUBKEY],
|
|
31
|
+
['i', query],
|
|
32
|
+
];
|
|
33
|
+
await event.publish();
|
|
34
|
+
return await this.waitForDVM(event, [KIND_SEARCH_RESULT, KIND_FEEDBACK]);
|
|
35
|
+
}
|
|
36
|
+
async requestSummary(marketId) {
|
|
37
|
+
const event = new NDKEvent(this.ndk);
|
|
38
|
+
event.kind = KIND_SUMMARY_REQUEST;
|
|
39
|
+
event.content = marketId;
|
|
40
|
+
event.tags = [
|
|
41
|
+
['p', DVM_PUBKEY],
|
|
42
|
+
['i', marketId],
|
|
43
|
+
];
|
|
44
|
+
await event.publish();
|
|
45
|
+
return this.waitForDVM(event, [KIND_SUMMARY_RESULT, KIND_FEEDBACK]);
|
|
46
|
+
}
|
|
47
|
+
async waitForDVM(requestEvent, kinds, timeoutMs = 60000) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
let resolved = false;
|
|
50
|
+
const filter = {
|
|
51
|
+
kinds: kinds,
|
|
52
|
+
'#e': [requestEvent.id],
|
|
53
|
+
authors: [DVM_PUBKEY],
|
|
54
|
+
};
|
|
55
|
+
const timeout = setTimeout(() => {
|
|
56
|
+
if (!resolved) {
|
|
57
|
+
resolved = true;
|
|
58
|
+
sub.stop();
|
|
59
|
+
reject(new Error(`Timeout waiting for DVM response (Event ID: ${requestEvent.id})`));
|
|
60
|
+
}
|
|
61
|
+
}, timeoutMs);
|
|
62
|
+
const sub = this.ndk.subscribe(filter, { closeOnEose: false });
|
|
63
|
+
sub.on('event', (event) => {
|
|
64
|
+
if (resolved)
|
|
65
|
+
return;
|
|
66
|
+
resolved = true;
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
sub.stop();
|
|
69
|
+
try {
|
|
70
|
+
if (event.kind === KIND_FEEDBACK) {
|
|
71
|
+
const statusTag = event.tags.find(t => t[0] === 'status');
|
|
72
|
+
const amountTag = event.tags.find(t => t[0] === 'amount');
|
|
73
|
+
resolve({
|
|
74
|
+
status: statusTag ? statusTag[1] : 'unknown',
|
|
75
|
+
amount: amountTag ? amountTag[1] : undefined,
|
|
76
|
+
invoice: amountTag ? amountTag[2] : undefined,
|
|
77
|
+
content: event.content
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
resolve(JSON.parse(event.content));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
reject(new Error(`Failed to parse DVM response: ${e}`));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async close() {
|
|
91
|
+
// NDK doesn't have a direct close, but we can stop all subs if needed.
|
|
92
|
+
// Relays are handled by NDK internally.
|
|
93
|
+
}
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "polymarket-dvm-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "ts-node src/index.ts",
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"serve": "node dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
21
|
+
"@nostr-dev-kit/ndk": "^3.0.0",
|
|
22
|
+
"@types/node": "^25.2.3",
|
|
23
|
+
"ts-node": "^10.9.2",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"ws": "^8.19.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/ws": "^8.18.1"
|
|
29
|
+
}
|
|
30
|
+
}
|