mcp-server-research-signals 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.
Files changed (2) hide show
  1. package/index.js +164 -0
  2. package/package.json +26 -0
package/index.js ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
4
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ const { z } = require("zod");
6
+ const https = require("https");
7
+ const http = require("http");
8
+
9
+ // ── Config ──────────────────────────────────────────────────────────
10
+
11
+ const API_KEY = process.env.MT_SIGNALS_API_KEY || "";
12
+ const BASE_URL = (process.env.MT_SIGNALS_URL || "https://signals.metaltorque.dev").replace(/\/$/, "");
13
+
14
+ // ── HTTP helper ─────────────────────────────────────────────────────
15
+
16
+ function request(method, urlPath, body, timeout = 30000) {
17
+ return new Promise((resolve, reject) => {
18
+ const sep = urlPath.includes("?") ? "&" : "?";
19
+ const fullUrl = API_KEY ? `${BASE_URL}${urlPath}${sep}key=${API_KEY}` : `${BASE_URL}${urlPath}`;
20
+ const mod = fullUrl.startsWith("https") ? https : http;
21
+ const parsed = new URL(fullUrl);
22
+
23
+ const headers = {
24
+ "Content-Type": "application/json",
25
+ "User-Agent": "mcp-server-research-signals/1.0.0",
26
+ };
27
+
28
+ const opts = {
29
+ hostname: parsed.hostname,
30
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
31
+ path: parsed.pathname + parsed.search,
32
+ method,
33
+ headers,
34
+ timeout,
35
+ };
36
+
37
+ const req = mod.request(opts, (res) => {
38
+ let data = "";
39
+ res.on("data", (c) => (data += c));
40
+ res.on("end", () => {
41
+ try {
42
+ const json = JSON.parse(data);
43
+ if (res.statusCode >= 400) return reject(new Error(json.error || `HTTP ${res.statusCode}`));
44
+ resolve(json);
45
+ } catch {
46
+ if (res.statusCode >= 400) return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 300)}`));
47
+ resolve(data);
48
+ }
49
+ });
50
+ });
51
+
52
+ req.on("error", reject);
53
+ req.on("timeout", () => { req.destroy(); reject(new Error("Request timed out")); });
54
+ if (body) req.write(JSON.stringify(body));
55
+ req.end();
56
+ });
57
+ }
58
+
59
+ // ── MCP Server ──────────────────────────────────────────────────────
60
+
61
+ const server = new McpServer({
62
+ name: "research-signals",
63
+ version: "1.0.0",
64
+ });
65
+
66
+ // ── Tool: list_signals ──────────────────────────────────────────────
67
+
68
+ server.tool(
69
+ "list_signals",
70
+ "List scored research signals from swarm intelligence. Each signal has a composite score based on urgency, buyer intent, durability, and source count. Signals come from HN, Reddit, ArXiv, GitHub, and other sources.",
71
+ {
72
+ category: z.string().optional().describe("Filter by category: ai, infrastructure, security, devtools, data, business, career, market"),
73
+ sort: z.string().optional().describe("Sort by: score (default), urgency, buyer_intent, durability"),
74
+ min_score: z.number().optional().describe("Minimum score filter"),
75
+ limit: z.number().default(25).describe("Max signals to return (default: 25)"),
76
+ },
77
+ async ({ category, sort, min_score, limit }) => {
78
+ const qs = [];
79
+ if (category) qs.push(`category=${encodeURIComponent(category)}`);
80
+ if (sort) qs.push(`sort=${sort}`);
81
+ if (min_score) qs.push(`min_score=${min_score}`);
82
+ if (limit) qs.push(`limit=${limit}`);
83
+ const query = qs.length > 0 ? `?${qs.join("&")}` : "";
84
+ const result = await request("GET", `/signals${query}`);
85
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
86
+ }
87
+ );
88
+
89
+ // ── Tool: trending_signals ──────────────────────────────────────────
90
+
91
+ server.tool(
92
+ "trending_signals",
93
+ "Get breakout signals (trending at 1.8x+ their rolling average) and hot signals (score 6+). Use this to find what's surging right now.",
94
+ {},
95
+ async () => {
96
+ const result = await request("GET", "/signals/trending");
97
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
98
+ }
99
+ );
100
+
101
+ // ── Tool: signal_detail ─────────────────────────────────────────────
102
+
103
+ server.tool(
104
+ "signal_detail",
105
+ "Get deep detail on a specific research signal including full evidence, score breakdown (urgency, buyer intent, durability), actionable-for audience, and trend history.",
106
+ {
107
+ topic: z.string().describe("The signal topic to look up (partial match supported, use hyphens for spaces)"),
108
+ },
109
+ async ({ topic }) => {
110
+ const encoded = encodeURIComponent(topic.replace(/\s+/g, "-"));
111
+ const result = await request("GET", `/signals/${encoded}`);
112
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
113
+ }
114
+ );
115
+
116
+ // ── Tool: signals_by_category ───────────────────────────────────────
117
+
118
+ server.tool(
119
+ "signals_by_category",
120
+ "Get all signals in a specific category. Categories: ai, infrastructure, security, devtools, data, business, career, market.",
121
+ {
122
+ category: z.string().describe("Category name"),
123
+ },
124
+ async ({ category }) => {
125
+ const result = await request("GET", `/signals/category/${encodeURIComponent(category)}`);
126
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
127
+ }
128
+ );
129
+
130
+ // ── Tool: signal_stats ──────────────────────────────────────────────
131
+
132
+ server.tool(
133
+ "signal_stats",
134
+ "Get aggregate statistics: total signals, category breakdown (count + avg score + top topic per category), and the top 10 signals overall.",
135
+ {},
136
+ async () => {
137
+ const result = await request("GET", "/stats");
138
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
139
+ }
140
+ );
141
+
142
+ // ── Tool: recalculate_scores ────────────────────────────────────────
143
+
144
+ server.tool(
145
+ "recalculate_scores",
146
+ "Force a recalculation of all signal scores. Applies time decay to urgency and detects new breakout trends. Use after ingesting new data.",
147
+ {},
148
+ async () => {
149
+ const result = await request("POST", "/trigger", { action: "recalculate" });
150
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
151
+ }
152
+ );
153
+
154
+ // ── Start ───────────────────────────────────────────────────────────
155
+
156
+ async function main() {
157
+ const transport = new StdioServerTransport();
158
+ await server.connect(transport);
159
+ }
160
+
161
+ main().catch((e) => {
162
+ console.error("MCP server error:", e);
163
+ process.exit(1);
164
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "mcp-server-research-signals",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for research signal intelligence — scored trends from HN, Reddit, ArXiv, and GitHub.",
5
+ "bin": {
6
+ "mcp-server-research-signals": "index.js"
7
+ },
8
+ "keywords": ["mcp", "research", "signals", "trends", "market-intelligence", "model-context-protocol"],
9
+ "license": "MIT",
10
+ "author": "MetalTorque",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/joepangallo/research-signal-api"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.27.0",
17
+ "zod": "^3.23.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "files": [
23
+ "index.js",
24
+ "README.md"
25
+ ]
26
+ }