src / mcpServer.ts
#!/usr/bin/env node
// Concise MCP server wrapper for LM Studio mcp.json compatibility
// Port of stefanoamorelli/fred-mcp-server - single file <200 LOC
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
const BASE = "https://api.stlouisfed.org/fred";
const API_KEY = process.env.FRED_API_KEY;
if (!API_KEY) {
console.error("FRED_API_KEY not set");
process.exit(1);
}
async function fred(endpoint: string, params: Record<string, any> = {}) {
const url = new URL(`${BASE}/${endpoint}`);
url.searchParams.set("api_key", API_KEY!);
url.searchParams.set("file_type", "json");
for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
const r = await fetch(url.toString());
if (!r.ok) throw new Error(`FRED ${r.status}: ${await r.text().then(t=>t.slice(0,500))}`);
return r.json();
}
const server = new McpServer({ name: "fred-mcp-lmstudio", version: "1.0.0" }, { capabilities: { tools: {} } });
server.tool(
"fred_browse",
"Browse FRED catalog: categories, releases, sources, category_series, release_series",
{
browse_type: z.enum(["categories","releases","sources","category_series","release_series"]),
category_id: z.number().optional(),
release_id: z.number().optional(),
limit: z.number().min(1).max(1000).optional(),
offset: z.number().min(0).optional(),
order_by: z.string().optional(),
sort_order: z.enum(["asc","desc"]).optional(),
},
async ({ browse_type, category_id, release_id, limit, offset, order_by, sort_order }) => {
const base: any = { limit: limit ?? 50, offset, order_by, sort_order };
let ep = "", p: any = { ...base };
if (browse_type === "categories") { ep = "category/children"; p.category_id = category_id ?? 0; }
else if (browse_type === "releases") ep = "releases";
else if (browse_type === "sources") ep = "sources";
else if (browse_type === "category_series") { if (category_id===undefined) throw new Error("category_id required"); ep="category/series"; p.category_id=category_id; }
else { if (release_id===undefined) throw new Error("release_id required"); ep="release/series"; p.release_id=release_id; }
const data = await fred(ep, p);
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2).slice(0,15000) }] };
}
);
server.tool(
"fred_search",
"Search FRED series by keywords/tags. Returns series IDs like GDP, UNRATE",
{
search_text: z.string().optional(),
search_type: z.string().optional(),
tag_names: z.string().optional(),
exclude_tag_names: z.string().optional(),
limit: z.number().min(1).max(1000).optional(),
offset: z.number().min(0).optional(),
order_by: z.string().optional(),
sort_order: z.enum(["asc","desc"]).optional(),
filter_variable: z.string().optional(),
filter_value: z.string().optional(),
},
async (args) => {
const data: any = await fred("series/search", { ...args, limit: args.limit ?? 25 });
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2).slice(0,15000) }] };
}
);
server.tool(
"fred_get_series",
"Get observations for a FRED series ID with transformations",
{
series_id: z.string(),
observation_start: z.string().optional(),
observation_end: z.string().optional(),
limit: z.number().optional(),
offset: z.number().optional(),
sort_order: z.enum(["asc","desc"]).optional(),
units: z.string().optional(),
frequency: z.string().optional(),
aggregation_method: z.string().optional(),
},
async (p) => {
const [meta, obs] = await Promise.all([
fred("series", { series_id: p.series_id }).catch(()=>({})),
fred("series/observations", { ...p, sort_order: p.sort_order ?? "asc" })
]);
return { content: [{ type: "text" as const, text: JSON.stringify({ meta: (meta as any).seriess?.[0], observations: (obs as any).observations?.slice(-200), count: (obs as any).observations?.length }, null, 2).slice(0,20000) }] };
}
);
async function main() {
const transportType = (process.env.TRANSPORT || process.argv.find(a=>a==="--http") ? "http" : "stdio").toLowerCase();
if (transportType === "http") {
const port = parseInt(process.env.PORT || "3000");
const { createServer } = await import("http");
const { randomUUID } = await import("crypto");
const sessions = new Map();
const httpServer = createServer(async (req, res) => {
if (req.url === "/mcp" && req.method === "POST") {
let body = "";
for await (const chunk of req) body += chunk;
const sessionId = req.headers["mcp-session-id"] as string || randomUUID();
let transport = sessions.get(sessionId);
if (!transport) {
transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => sessionId });
await server.connect(transport);
sessions.set(sessionId, transport);
}
// @ts-ignore simplified handling
await transport.handleRequest(req, res, JSON.parse(body));
} else {
res.writeHead(404).end();
}
});
httpServer.listen(port, () => console.error(`FRED MCP HTTP at http://localhost:${port}/mcp`));
} else {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("FRED MCP stdio started");
}
}
main().catch(e=>{ console.error(e); process.exit(1); });
src / mcpServer.ts
#!/usr/bin/env node
// Concise MCP server wrapper for LM Studio mcp.json compatibility
// Port of stefanoamorelli/fred-mcp-server - single file <200 LOC
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
const BASE = "https://api.stlouisfed.org/fred";
const API_KEY = process.env.FRED_API_KEY;
if (!API_KEY) {
console.error("FRED_API_KEY not set");
process.exit(1);
}
async function fred(endpoint: string, params: Record<string, any> = {}) {
const url = new URL(`${BASE}/${endpoint}`);
url.searchParams.set("api_key", API_KEY!);
url.searchParams.set("file_type", "json");
for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
const r = await fetch(url.toString());
if (!r.ok) throw new Error(`FRED ${r.status}: ${await r.text().then(t=>t.slice(0,500))}`);
return r.json();
}
const server = new McpServer({ name: "fred-mcp-lmstudio", version: "1.0.0" }, { capabilities: { tools: {} } });
server.tool(
"fred_browse",
"Browse FRED catalog: categories, releases, sources, category_series, release_series",
{
browse_type: z.enum(["categories","releases","sources","category_series","release_series"]),
category_id: z.number().optional(),
release_id: z.number().optional(),
limit: z.number().min(1).max(1000).optional(),
offset: z.number().min(0).optional(),
order_by: z.string().optional(),
sort_order: z.enum(["asc","desc"]).optional(),
},
async ({ browse_type, category_id, release_id, limit, offset, order_by, sort_order }) => {
const base: any = { limit: limit ?? 50, offset, order_by, sort_order };
let ep = "", p: any = { ...base };
if (browse_type === "categories") { ep = "category/children"; p.category_id = category_id ?? 0; }
else if (browse_type === "releases") ep = "releases";
else if (browse_type === "sources") ep = "sources";
else if (browse_type === "category_series") { if (category_id===undefined) throw new Error("category_id required"); ep="category/series"; p.category_id=category_id; }
else { if (release_id===undefined) throw new Error("release_id required"); ep="release/series"; p.release_id=release_id; }
const data = await fred(ep, p);
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2).slice(0,15000) }] };
}
);
server.tool(
"fred_search",
"Search FRED series by keywords/tags. Returns series IDs like GDP, UNRATE",
{
search_text: z.string().optional(),
search_type: z.string().optional(),
tag_names: z.string().optional(),
exclude_tag_names: z.string().optional(),
limit: z.number().min(1).max(1000).optional(),
offset: z.number().min(0).optional(),
order_by: z.string().optional(),
sort_order: z.enum(["asc","desc"]).optional(),
filter_variable: z.string().optional(),
filter_value: z.string().optional(),
},
async (args) => {
const data: any = await fred("series/search", { ...args, limit: args.limit ?? 25 });
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2).slice(0,15000) }] };
}
);
server.tool(
"fred_get_series",
"Get observations for a FRED series ID with transformations",
{
series_id: z.string(),
observation_start: z.string().optional(),
observation_end: z.string().optional(),
limit: z.number().optional(),
offset: z.number().optional(),
sort_order: z.enum(["asc","desc"]).optional(),
units: z.string().optional(),
frequency: z.string().optional(),
aggregation_method: z.string().optional(),
},
async (p) => {
const [meta, obs] = await Promise.all([
fred("series", { series_id: p.series_id }).catch(()=>({})),
fred("series/observations", { ...p, sort_order: p.sort_order ?? "asc" })
]);
return { content: [{ type: "text" as const, text: JSON.stringify({ meta: (meta as any).seriess?.[0], observations: (obs as any).observations?.slice(-200), count: (obs as any).observations?.length }, null, 2).slice(0,20000) }] };
}
);
async function main() {
const transportType = (process.env.TRANSPORT || process.argv.find(a=>a==="--http") ? "http" : "stdio").toLowerCase();
if (transportType === "http") {
const port = parseInt(process.env.PORT || "3000");
const { createServer } = await import("http");
const { randomUUID } = await import("crypto");
const sessions = new Map();
const httpServer = createServer(async (req, res) => {
if (req.url === "/mcp" && req.method === "POST") {
let body = "";
for await (const chunk of req) body += chunk;
const sessionId = req.headers["mcp-session-id"] as string || randomUUID();
let transport = sessions.get(sessionId);
if (!transport) {
transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => sessionId });
await server.connect(transport);
sessions.set(sessionId, transport);
}
// @ts-ignore simplified handling
await transport.handleRequest(req, res, JSON.parse(body));
} else {
res.writeHead(404).end();
}
});
httpServer.listen(port, () => console.error(`FRED MCP HTTP at http://localhost:${port}/mcp`));
} else {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("FRED MCP stdio started");
}
}
main().catch(e=>{ console.error(e); process.exit(1); });