Skip to content

Commit 43b8aca

Browse files
Add MCP stdio server + Dockerfile for Glama inspection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 648e8f0 commit 43b8aca

4 files changed

Lines changed: 1188 additions & 2 deletions

File tree

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM node:22-slim
2+
3+
WORKDIR /app
4+
5+
COPY package.json package-lock.json ./
6+
RUN npm ci --ignore-scripts
7+
8+
COPY mcp-server.mjs ./
9+
10+
ENTRYPOINT ["node", "mcp-server.mjs"]

mcp-server.mjs

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* SimpleFunctions MCP Server (stdio transport)
5+
*
6+
* Thin stdio wrapper for Glama inspection + local MCP clients.
7+
* All tools proxy to https://simplefunctions.dev/api/*
8+
*
9+
* Usage:
10+
* SF_API_KEY=sf_live_xxx node mcp-server.mjs
11+
*
12+
* Or connect to the hosted Streamable HTTP endpoint directly:
13+
* https://simplefunctions.dev/api/mcp/mcp
14+
*/
15+
16+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18+
import { z } from 'zod';
19+
20+
const BASE = 'https://simplefunctions.dev';
21+
22+
async function api(path, opts = {}) {
23+
const apiKey = process.env.SF_API_KEY;
24+
const headers = { 'Content-Type': 'application/json', ...opts.headers };
25+
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
26+
const res = await fetch(`${BASE}${path}`, { ...opts, headers });
27+
return res.json();
28+
}
29+
30+
const server = new McpServer({
31+
name: 'SimpleFunctions',
32+
version: '1.0.0',
33+
});
34+
35+
// ── Public tools (no auth) ─────────────────────────────────
36+
37+
server.tool(
38+
'get_context',
39+
'START HERE. Global market snapshot: top edges (mispriced contracts), price movers, highlights, traditional markets. With thesisId + apiKey: thesis-specific context including causal tree and evaluation history.',
40+
{
41+
thesisId: z.string().optional().describe('Thesis ID. Omit for global market snapshot.'),
42+
apiKey: z.string().optional().describe('SF API key. Required for thesis-specific context.'),
43+
},
44+
async ({ thesisId, apiKey: key }) => {
45+
if (!thesisId) {
46+
const data = await api('/api/public/context');
47+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
48+
}
49+
const data = await fetch(`${BASE}/api/thesis/${thesisId}/context`, {
50+
headers: { 'Authorization': `Bearer ${key || process.env.SF_API_KEY}` },
51+
}).then(r => r.json());
52+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
53+
}
54+
);
55+
56+
server.tool(
57+
'get_world_state',
58+
'Calibrated world model: 9,706 prediction markets distilled into 800 tokens. Real-money probabilities on geopolitics, economics, tech, policy.',
59+
{
60+
focus: z.string().optional().describe('Comma-separated topics: energy,geo,tech,policy,crypto,finance'),
61+
format: z.enum(['markdown', 'json']).default('markdown').optional(),
62+
},
63+
async ({ focus, format }) => {
64+
const params = new URLSearchParams();
65+
if (focus) params.set('focus', focus);
66+
if (format) params.set('format', format);
67+
const qs = params.toString();
68+
const data = await fetch(`${BASE}/api/agent/world${qs ? '?' + qs : ''}`).then(r => r.text());
69+
return { content: [{ type: 'text', text: data }] };
70+
}
71+
);
72+
73+
server.tool(
74+
'get_world_delta',
75+
'What changed since a timestamp. ~30-50 tokens vs 800 for full state.',
76+
{
77+
since: z.string().describe('Relative (30m, 1h, 6h, 24h) or ISO timestamp'),
78+
format: z.enum(['markdown', 'json']).default('markdown').optional(),
79+
},
80+
async ({ since, format }) => {
81+
let url = `${BASE}/api/agent/world/delta?since=${encodeURIComponent(since)}`;
82+
if (format) url += `&format=${format}`;
83+
const data = await fetch(url).then(r => r.text());
84+
return { content: [{ type: 'text', text: data }] };
85+
}
86+
);
87+
88+
server.tool(
89+
'get_markets',
90+
'Live prediction market contracts with prices, volume, and metadata. Filter by topic for deep dives.',
91+
{
92+
topic: z.string().optional().describe('Filter: energy, rates, fx, equities, crypto, volatility'),
93+
limit: z.number().default(20).optional(),
94+
},
95+
async ({ topic, limit }) => {
96+
const params = new URLSearchParams();
97+
if (topic) params.set('topic', topic);
98+
if (limit) params.set('limit', String(limit));
99+
const data = await api(`/api/public/markets?${params}`);
100+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
101+
}
102+
);
103+
104+
server.tool(
105+
'search_markets',
106+
'Search prediction market contracts by keyword.',
107+
{ query: z.string().describe('Search query') },
108+
async ({ query }) => {
109+
const data = await api(`/api/public/markets?q=${encodeURIComponent(query)}`);
110+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
111+
}
112+
);
113+
114+
server.tool(
115+
'get_changes',
116+
'Biggest price movers in the last 24h across all prediction markets.',
117+
{},
118+
async () => {
119+
const data = await api('/api/public/changes');
120+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
121+
}
122+
);
123+
124+
server.tool(
125+
'get_edges',
126+
'Current mispricings detected across all public theses.',
127+
{},
128+
async () => {
129+
const data = await api('/api/edges');
130+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
131+
}
132+
);
133+
134+
server.tool(
135+
'get_trade_ideas',
136+
'AI-generated trade ideas based on thesis analysis and market data.',
137+
{},
138+
async () => {
139+
const data = await api('/api/public/ideas');
140+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
141+
}
142+
);
143+
144+
server.tool(
145+
'enrich_content',
146+
'Free: paste any text, get prediction market cross-reference. No auth needed.',
147+
{
148+
content: z.string().describe('Text content to analyze (max 50,000 chars)'),
149+
topics: z.array(z.string()).describe('Topics to search for in prediction markets'),
150+
model: z.string().optional().describe('LLM model for digest (default: gemini-2.5-flash)'),
151+
},
152+
async ({ content, topics, model }) => {
153+
const data = await fetch(`${BASE}/api/monitor-the-situation/enrich`, {
154+
method: 'POST',
155+
headers: { 'Content-Type': 'application/json' },
156+
body: JSON.stringify({ content, topics, model }),
157+
}).then(r => r.json());
158+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
159+
}
160+
);
161+
162+
// ── Auth-required tools ────────────────────────────────────
163+
164+
server.tool(
165+
'list_theses',
166+
'List all theses for the authenticated user.',
167+
{ apiKey: z.string().describe('SF API key') },
168+
async ({ apiKey: key }) => {
169+
const data = await fetch(`${BASE}/api/thesis`, {
170+
headers: { 'Authorization': `Bearer ${key}` },
171+
}).then(r => r.json());
172+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
173+
}
174+
);
175+
176+
server.tool(
177+
'inject_signal',
178+
'Feed an observation into a thesis for next evaluation cycle.',
179+
{
180+
thesisId: z.string(),
181+
apiKey: z.string(),
182+
content: z.string().describe('Signal content'),
183+
type: z.enum(['news', 'user_note', 'external']).default('user_note'),
184+
},
185+
async ({ thesisId, apiKey: key, content, type }) => {
186+
const data = await fetch(`${BASE}/api/thesis/${thesisId}/signal`, {
187+
method: 'POST',
188+
headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
189+
body: JSON.stringify({ type, content, source: 'mcp' }),
190+
}).then(r => r.json());
191+
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
192+
}
193+
);
194+
195+
server.tool(
196+
'trigger_evaluation',
197+
'Force immediate thesis evaluation: consume signals, re-scan edges, update confidence.',
198+
{
199+
thesisId: z.string(),
200+
apiKey: z.string(),
201+
},
202+
async ({ thesisId, apiKey: key }) => {
203+
const data = await fetch(`${BASE}/api/thesis/${thesisId}/evaluate`, {
204+
method: 'POST',
205+
headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
206+
body: JSON.stringify({}),
207+
}).then(r => r.json());
208+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
209+
}
210+
);
211+
212+
server.tool(
213+
'create_thesis',
214+
'Create a new thesis with a natural language statement.',
215+
{
216+
apiKey: z.string(),
217+
title: z.string().describe('Thesis statement'),
218+
metadata: z.record(z.unknown()).optional(),
219+
},
220+
async ({ apiKey: key, title, metadata }) => {
221+
const data = await fetch(`${BASE}/api/thesis/create`, {
222+
method: 'POST',
223+
headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
224+
body: JSON.stringify({ title, metadata }),
225+
}).then(r => r.json());
226+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
227+
}
228+
);
229+
230+
server.tool(
231+
'fork_thesis',
232+
'Fork a public thesis to your account.',
233+
{
234+
apiKey: z.string(),
235+
idOrSlug: z.string().describe('Thesis ID or public slug'),
236+
},
237+
async ({ apiKey: key, idOrSlug }) => {
238+
const data = await fetch(`${BASE}/api/thesis/${idOrSlug}/fork`, {
239+
method: 'POST',
240+
headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
241+
body: JSON.stringify({}),
242+
}).then(r => r.json());
243+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
244+
}
245+
);
246+
247+
server.tool(
248+
'monitor_the_situation',
249+
'Scrape any URL, analyze with LLM, cross-reference with prediction markets, push to webhook.',
250+
{
251+
apiKey: z.string(),
252+
source: z.object({
253+
action: z.enum(['scrape', 'crawl', 'search', 'map', 'extract', 'batch_scrape']),
254+
url: z.string().optional(),
255+
urls: z.array(z.string()).optional(),
256+
query: z.string().optional(),
257+
options: z.record(z.unknown()).optional(),
258+
}),
259+
analysis: z.object({
260+
enabled: z.boolean(),
261+
prompt: z.string(),
262+
model: z.string().optional(),
263+
schema: z.record(z.unknown()).optional(),
264+
}).optional(),
265+
enrich: z.object({
266+
enabled: z.boolean(),
267+
topics: z.array(z.string()),
268+
}).optional(),
269+
},
270+
async ({ apiKey: key, source, analysis, enrich }) => {
271+
const data = await fetch(`${BASE}/api/monitor-the-situation`, {
272+
method: 'POST',
273+
headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
274+
body: JSON.stringify({ source, analysis, enrich }),
275+
}).then(r => r.json());
276+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
277+
}
278+
);
279+
280+
server.tool(
281+
'query_databento',
282+
'Query real-time and historical market data from Databento (CME futures, equities, crypto).',
283+
{
284+
symbols: z.array(z.string()).describe('Symbols like CL.c.0, ES.c.0, AAPL'),
285+
dataset: z.string().default('GLBX.MDP3').optional(),
286+
schema: z.string().default('trades').optional(),
287+
start: z.string().optional().describe('ISO date'),
288+
end: z.string().optional().describe('ISO date'),
289+
limit: z.number().default(100).optional(),
290+
},
291+
async ({ symbols, dataset, schema, start, end, limit }) => {
292+
const params = new URLSearchParams();
293+
symbols.forEach(s => params.append('symbols', s));
294+
if (dataset) params.set('dataset', dataset);
295+
if (schema) params.set('schema', schema);
296+
if (start) params.set('start', start);
297+
if (end) params.set('end', end);
298+
if (limit) params.set('limit', String(limit));
299+
const data = await api(`/api/public/databento?${params}`);
300+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
301+
}
302+
);
303+
304+
// ── Start ──────────────────────────────────────────────────
305+
306+
const transport = new StdioServerTransport();
307+
await server.connect(transport);

0 commit comments

Comments
 (0)