@tickerbot/mcp-server 0.3.1 → 0.4.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Model Context Protocol server for [Tickerbot](https://tickerbot.io) — the stock market, in SQL. Install in Claude, Cursor, VS Code Copilot, and other MCP-compatible clients to scan ~14,500 US-listed equities plus a curated set of rates, FX and crypto series, replay any day, or subscribe a query — across 421+ signal columns.
4
4
 
5
+ This package is a thin stdio bridge to the hosted Tickerbot MCP server at `https://api.tickerbot.io/mcp`. On start it reads the tool list from that server with your key, and it forwards every tool call there and returns the result unchanged. A tool added or changed on Tickerbot reaches you the next time your client starts the server, with no package update. Each request to the hosted server carries an `X-Tickerbot-MCP-Client: @tickerbot/mcp-server/<version>` header, so Tickerbot's call log can tell this package's traffic from other MCP clients; it is a label only.
6
+
5
7
  ## Install
6
8
 
7
9
  You need a Tickerbot API key. Get one from your [dashboard](https://tickerbot.io/dashboard).
@@ -66,7 +68,7 @@ Add to `.vscode/mcp.json` in your workspace:
66
68
 
67
69
  ## Tools
68
70
 
69
- 33 tools covering scans, signals, tickers (state, history, bars, coverage, ETF holdings), events and their kinds, news, universes and webhook subscriptions (scan, signal, ticker, events and news). Bars and History answer `data`: one symbol gives its rows, a comma list gives them keyed by symbol.
71
+ The tools are whatever the hosted server lists when the bridge starts. At the time of this release that is 33 tools covering scans, signals, tickers (state, history, bars, coverage, ETF holdings), events and their kinds, news, universes and webhook subscriptions (scan, signal, ticker, events and news). Bars and History answer `data`: one symbol gives its rows, a comma list gives them keyed by symbol.
70
72
 
71
73
  | Tool | Endpoint |
72
74
  | --- | --- |
@@ -106,6 +108,10 @@ Add to `.vscode/mcp.json` in your workspace:
106
108
 
107
109
  Strategies are not exposed via MCP — see the [REST API docs](https://tickerbot.io/api) for those.
108
110
 
111
+ ### When the hosted server can't be reached
112
+
113
+ If `api.tickerbot.io/mcp` does not answer at start, the bridge serves the tool list bundled with this version, runs each call itself against the REST API, and writes a warning to stderr. The bundled list can be behind the server's. The bridge retries the server at most once a minute (on `tools/list`) and switches back when it answers. With no `TICKERBOT_API_KEY` set it does not contact the server: it lists the bundled tools and every call answers with how to set a key.
114
+
109
115
  ## Examples
110
116
 
111
117
  Open Claude and try:
@@ -122,12 +128,20 @@ Open Claude and try:
122
128
 
123
129
  | Env var | Default | Purpose |
124
130
  | --- | --- | --- |
125
- | `TICKERBOT_API_KEY` | *(required)* | Your API key. Sent as `Authorization: Bearer <key>` on every call. |
126
- | `TICKERBOT_API_URL` | `https://api.tickerbot.io` | Override the API base URL (for staging or self-hosted). |
131
+ | `TICKERBOT_API_KEY` | *(required)* | Your API key. Sent as `Authorization: Bearer <key>` to the hosted MCP server (and to the REST API in fallback). |
132
+ | `TICKERBOT_API_URL` | `https://api.tickerbot.io` | Override the base URL. The bridge connects to `<base>/mcp`; the fallback calls `<base>/v2/...`. |
127
133
 
128
134
  ## Remote install
129
135
 
130
- For consumer chat apps (Claude.ai web/mobile, ChatGPT) that don't run subprocesses, use the hosted remote MCP endpoint at `https://mcp.tickerbot.io` instead. See [tickerbot.io/mcp-server](https://tickerbot.io/mcp-server) for setup.
136
+ Clients that connect to an MCP server by URL (Claude.ai web/mobile, ChatGPT, and most desktop and IDE clients) can use the hosted server at `https://api.tickerbot.io/mcp` directly and sign in with OAuth, no package needed. See [tickerbot.io/docs/mcp](https://tickerbot.io/docs/mcp) for setup.
137
+
138
+ ## Development
139
+
140
+ ```bash
141
+ npm install
142
+ npm test # builds, then runs the bridge against a stubbed hosted server
143
+ node scripts/regen-tools.mjs # refresh the bundled fallback list from ../tickerbot (optional)
144
+ ```
131
145
 
132
146
  ## License
133
147
 
package/dist/handler.d.ts CHANGED
@@ -4,9 +4,7 @@ export interface RunToolOptions {
4
4
  baseUrl?: string;
5
5
  /** Optional user-agent appended to identify the MCP server. */
6
6
  userAgent?: string;
7
- /** Override the tool lookup. Used when index.ts has fetched the live
8
- * catalog from /mcp/tools — we dispatch against the server's
9
- * current tool definitions, not the baked snapshot. */
7
+ /** Override the tool lookup (defaults to the bundled snapshot). */
10
8
  findTool?: (name: string) => ToolDef | undefined;
11
9
  }
12
10
  export declare class ToolError extends Error {
package/dist/handler.js CHANGED
@@ -1,10 +1,10 @@
1
- // Dispatches an MCP tool call to the Tickerbot REST API.
1
+ // Dispatches an MCP tool call to the Tickerbot REST API, against the
2
+ // bundled snapshot (src/tools.ts).
2
3
  //
3
- // Same handler is used by both transports:
4
- // - the stdio binary (src/index.ts) for local desktop/IDE clients
5
- // - the HTTP endpoint on main_service for remote chat clients
6
- //
7
- // One source of truth for path substitution, query/body assembly, auth.
4
+ // Fallback only (TB-740): the stdio bridge (src/index.ts) forwards calls to
5
+ // the hosted MCP server and uses this runner only while that server cannot
6
+ // be reached, or when no key is set. Mirrors main_service/mcp/handler.js,
7
+ // which runs every call the hosted server answers.
8
8
  import { findTool as bakedFindTool } from './tools.js';
9
9
  const DEFAULT_BASE_URL = 'https://api.tickerbot.io';
10
10
  export class ToolError extends Error {
@@ -0,0 +1,52 @@
1
+ export interface HostedTool {
2
+ name: string;
3
+ description?: string;
4
+ inputSchema: {
5
+ type: 'object';
6
+ [k: string]: unknown;
7
+ };
8
+ outputSchema?: {
9
+ type: 'object';
10
+ [k: string]: unknown;
11
+ };
12
+ [k: string]: unknown;
13
+ }
14
+ export interface HostedCallResult {
15
+ content: unknown[];
16
+ structuredContent?: Record<string, unknown>;
17
+ isError?: boolean;
18
+ [k: string]: unknown;
19
+ }
20
+ /** The server could not be reached or did not answer with JSON-RPC. */
21
+ export declare class HostedUnavailable extends Error {
22
+ readonly status?: number | undefined;
23
+ constructor(message: string, status?: number | undefined);
24
+ }
25
+ /** The server answered with a JSON-RPC error object. */
26
+ export declare class HostedRpcError extends Error {
27
+ readonly code: number;
28
+ readonly data?: unknown | undefined;
29
+ constructor(code: number, message: string, data?: unknown | undefined);
30
+ }
31
+ export interface HostedClientOptions {
32
+ url: string;
33
+ apiKey: string;
34
+ userAgent: string;
35
+ clientVersion: string;
36
+ /** Sent as X-Tickerbot-MCP-Client: `@tickerbot/mcp-server/<version>`. */
37
+ clientLabel: string;
38
+ }
39
+ export declare class HostedClient {
40
+ private readonly opts;
41
+ private nextId;
42
+ private sessionId;
43
+ private protocolVersion;
44
+ constructor(opts: HostedClientOptions);
45
+ /** initialize + notifications/initialized + tools/list. */
46
+ connect(timeoutMs: number): Promise<HostedTool[]>;
47
+ listTools(timeoutMs: number): Promise<HostedTool[]>;
48
+ callTool(name: string, args: Record<string, unknown>, timeoutMs: number): Promise<HostedCallResult>;
49
+ private notify;
50
+ private request;
51
+ private post;
52
+ }
package/dist/hosted.js ADDED
@@ -0,0 +1,158 @@
1
+ // Client for the hosted Tickerbot MCP server ({baseUrl}/mcp, served by
2
+ // main_service/endpoints/mcp.js).
3
+ //
4
+ // The hosted server speaks MCP over Streamable HTTP in its single-JSON form:
5
+ // every JSON-RPC message is one POST to /mcp with `Authorization: Bearer
6
+ // <key>`, and the answer is one JSON body (a notification gets 202 and no
7
+ // body). It keeps no session and never streams, so this client is a plain
8
+ // fetch per message. It still behaves as a spec client (Accept lists
9
+ // text/event-stream, an mcp-session-id is echoed back if one is issued, an
10
+ // SSE answer is read) so a later server change does not break old installs.
11
+ //
12
+ // No key, or a key the server refuses outright, answers HTTP 401 on every
13
+ // request. A key the server accepts at the door but the REST API refuses
14
+ // (malformed, revoked) passes initialize and tools/list; its tools/call
15
+ // comes back as an isError result carrying the REST 401 text, which the
16
+ // bridge returns unchanged.
17
+ /** The server could not be reached or did not answer with JSON-RPC. */
18
+ export class HostedUnavailable extends Error {
19
+ status;
20
+ constructor(message, status) {
21
+ super(message);
22
+ this.status = status;
23
+ this.name = 'HostedUnavailable';
24
+ }
25
+ }
26
+ /** The server answered with a JSON-RPC error object. */
27
+ export class HostedRpcError extends Error {
28
+ code;
29
+ data;
30
+ constructor(code, message, data) {
31
+ super(message);
32
+ this.code = code;
33
+ this.data = data;
34
+ this.name = 'HostedRpcError';
35
+ }
36
+ }
37
+ const CLIENT_PROTOCOL_VERSION = '2025-06-18';
38
+ export class HostedClient {
39
+ opts;
40
+ nextId = 1;
41
+ sessionId;
42
+ protocolVersion;
43
+ constructor(opts) {
44
+ this.opts = opts;
45
+ }
46
+ /** initialize + notifications/initialized + tools/list. */
47
+ async connect(timeoutMs) {
48
+ const init = (await this.request('initialize', {
49
+ protocolVersion: CLIENT_PROTOCOL_VERSION,
50
+ capabilities: {},
51
+ clientInfo: { name: this.opts.userAgent, version: this.opts.clientVersion },
52
+ }, timeoutMs));
53
+ if (init && typeof init.protocolVersion === 'string')
54
+ this.protocolVersion = init.protocolVersion;
55
+ await this.notify('notifications/initialized', timeoutMs);
56
+ return this.listTools(timeoutMs);
57
+ }
58
+ async listTools(timeoutMs) {
59
+ const result = (await this.request('tools/list', {}, timeoutMs));
60
+ if (!result || !Array.isArray(result.tools) || result.tools.length === 0) {
61
+ throw new HostedUnavailable('tools/list returned no tools');
62
+ }
63
+ return result.tools;
64
+ }
65
+ async callTool(name, args, timeoutMs) {
66
+ const result = (await this.request('tools/call', { name, arguments: args }, timeoutMs));
67
+ if (!result || !Array.isArray(result.content)) {
68
+ throw new HostedUnavailable('tools/call returned no content');
69
+ }
70
+ return result;
71
+ }
72
+ async notify(method, timeoutMs) {
73
+ await this.post({ jsonrpc: '2.0', method }, timeoutMs);
74
+ }
75
+ async request(method, params, timeoutMs) {
76
+ const id = this.nextId++;
77
+ const message = await this.post({ jsonrpc: '2.0', id, method, params }, timeoutMs, id);
78
+ if (!message)
79
+ throw new HostedUnavailable(`${method}: no response`);
80
+ if (message.error) {
81
+ const e = message.error;
82
+ throw new HostedRpcError(typeof e.code === 'number' ? e.code : -32603, e.message ?? 'Unknown error', e.data);
83
+ }
84
+ return message.result;
85
+ }
86
+ async post(body, timeoutMs, id) {
87
+ const headers = {
88
+ Authorization: `Bearer ${this.opts.apiKey}`,
89
+ 'Content-Type': 'application/json',
90
+ Accept: 'application/json, text/event-stream',
91
+ 'User-Agent': this.opts.userAgent,
92
+ // Names this client on every request (TB-740). The hosted server
93
+ // forwards it on its REST calls so the call log can tell npm-bridge
94
+ // usage from other clients; it must match `@tickerbot/mcp-server/<semver>`.
95
+ 'X-Tickerbot-MCP-Client': this.opts.clientLabel,
96
+ };
97
+ if (this.sessionId)
98
+ headers['mcp-session-id'] = this.sessionId;
99
+ if (this.protocolVersion)
100
+ headers['mcp-protocol-version'] = this.protocolVersion;
101
+ let res;
102
+ try {
103
+ res = await fetch(this.opts.url, {
104
+ method: 'POST',
105
+ headers,
106
+ body: JSON.stringify(body),
107
+ signal: AbortSignal.timeout(timeoutMs),
108
+ });
109
+ }
110
+ catch (err) {
111
+ const reason = err instanceof Error ? (err.name === 'TimeoutError' ? `timed out after ${timeoutMs} ms` : err.message) : String(err);
112
+ throw new HostedUnavailable(`${this.opts.url}: ${reason}`);
113
+ }
114
+ const session = res.headers.get('mcp-session-id');
115
+ if (session)
116
+ this.sessionId = session;
117
+ if (res.status === 202)
118
+ return null;
119
+ if (!res.ok) {
120
+ const text = await res.text().catch(() => '');
121
+ throw new HostedUnavailable(`${this.opts.url} answered ${res.status}${text ? `: ${text.slice(0, 300)}` : ''}`, res.status);
122
+ }
123
+ if (id === undefined) {
124
+ await res.body?.cancel().catch(() => { });
125
+ return null;
126
+ }
127
+ const type = res.headers.get('content-type') ?? '';
128
+ const text = await res.text();
129
+ const candidates = type.includes('text/event-stream') ? parseSse(text) : [parseJson(text)];
130
+ for (const c of candidates.flatMap((m) => (Array.isArray(m) ? m : [m]))) {
131
+ if (c && typeof c === 'object' && c.id === id)
132
+ return c;
133
+ }
134
+ throw new HostedUnavailable(`${this.opts.url}: no JSON-RPC response for id ${id}`);
135
+ }
136
+ }
137
+ function parseJson(text) {
138
+ try {
139
+ return JSON.parse(text);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ // Each SSE event's data lines joined, parsed as JSON.
146
+ function parseSse(text) {
147
+ const out = [];
148
+ for (const event of text.split(/\r?\n\r?\n/)) {
149
+ const data = event
150
+ .split(/\r?\n/)
151
+ .filter((l) => l.startsWith('data:'))
152
+ .map((l) => l.slice(5).replace(/^ /, ''))
153
+ .join('\n');
154
+ if (data)
155
+ out.push(parseJson(data));
156
+ }
157
+ return out;
158
+ }
package/dist/index.js CHANGED
@@ -1,82 +1,117 @@
1
- // Stdio MCP server entrypoint.
1
+ // Stdio MCP server entrypoint: a thin bridge to the hosted Tickerbot MCP
2
+ // server (TB-740).
2
3
  //
3
- // Local install target. Launched by the user's MCP client (Claude Desktop,
4
- // Claude Code, Cursor, VS Code Copilot, etc.) as a subprocess that talks
5
- // JSON-RPC over stdin/stdout.
4
+ // Launched by the user's MCP client (Claude Desktop, Claude Code, Cursor,
5
+ // VS Code Copilot, etc.) as a subprocess that talks JSON-RPC over
6
+ // stdin/stdout. The key comes from TICKERBOT_API_KEY.
6
7
  //
7
- // Reads the API key from TICKERBOT_API_KEY and forwards every tool call
8
- // through src/handler.ts → api.tickerbot.io.
8
+ // On start it connects to {baseUrl}/mcp (https://api.tickerbot.io/mcp) with
9
+ // the key, and the tool list it serves is the hosted server's tools/list,
10
+ // verbatim. Every tools/call is forwarded there and its result returned
11
+ // unchanged. So a tool added or changed on main_service reaches npm users on
12
+ // their client's next launch, with no npm publish.
9
13
  //
10
- // Tool catalog: at startup, this subprocess fetches the live catalog from
11
- // {baseUrl}/mcp/tools. main_service is the single source of truth — any
12
- // add/rename/description-tweak on the server side shows up here on the
13
- // next subprocess spawn (which means the next Claude Desktop relaunch).
14
- // If the fetch fails (offline, server down, network error), we fall back
15
- // to the baked snapshot in tools.ts so the package still works.
14
+ // Fallback: if the hosted server cannot be reached at start, the bridge
15
+ // serves the bundled snapshot (src/tools.ts) and runs calls itself against
16
+ // the REST API (src/handler.ts), and says so on stderr. While in fallback it
17
+ // retries the hosted server on tools/list, at most once a minute, and
18
+ // switches over when it answers.
19
+ //
20
+ // With no key the bridge does not contact the server (it would answer 401):
21
+ // it serves the snapshot, and every call answers the missing-key message.
16
22
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
17
23
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
24
+ import { CallToolRequestSchema, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
19
25
  import { tools as bakedTools } from './tools.js';
20
26
  import { runTool, ToolError } from './handler.js';
27
+ import { HostedClient, HostedRpcError } from './hosted.js';
21
28
  import { createRequire } from 'node:module';
22
29
  // package.json is the one place the version lives; the hardcoded '0.1.0'
23
- // this replaced was reported to every client through 0.2.1.
24
- const { name: SERVER_NAME, version: SERVER_VERSION } = createRequire(import.meta.url)('../package.json');
30
+ // this replaced was reported to every client through 0.2.1. The mcpb bundle
31
+ // is CJS with no import.meta.url and no package.json beside it, so
32
+ // scripts/build-mcpb.sh inlines the two fields as TICKERBOT_MCP_PACKAGE.
33
+ const { name: SERVER_NAME, version: SERVER_VERSION } = (process.env.TICKERBOT_MCP_PACKAGE
34
+ ? JSON.parse(process.env.TICKERBOT_MCP_PACKAGE)
35
+ : createRequire(import.meta.url)('../package.json'));
25
36
  const DEFAULT_BASE_URL = 'https://api.tickerbot.io';
26
- const DISCOVERY_TIMEOUT_MS = 3000;
27
- async function fetchLiveCatalog(baseUrl) {
28
- const url = `${baseUrl}/mcp/tools`;
29
- try {
30
- const controller = new AbortController();
31
- const timer = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT_MS);
32
- const res = await fetch(url, {
33
- headers: { Accept: 'application/json', 'User-Agent': '@tickerbot/mcp-server' },
34
- signal: controller.signal,
35
- });
36
- clearTimeout(timer);
37
- if (!res.ok) {
38
- process.stderr.write(`[tickerbot-mcp] discovery: ${url} returned ${res.status}; using baked snapshot\n`);
39
- return null;
40
- }
41
- const body = (await res.json());
42
- if (!Array.isArray(body.tools) || body.tools.length === 0) {
43
- process.stderr.write(`[tickerbot-mcp] discovery: malformed response from ${url}; using baked snapshot\n`);
44
- return null;
45
- }
46
- return body.tools;
47
- }
48
- catch (err) {
49
- const message = err instanceof Error ? err.message : String(err);
50
- process.stderr.write(`[tickerbot-mcp] discovery: ${url} failed (${message}); using baked snapshot\n`);
51
- return null;
52
- }
37
+ const CONNECT_TIMEOUT_MS = Number(process.env.TICKERBOT_MCP_CONNECT_TIMEOUT_MS) || 5000;
38
+ const CALL_TIMEOUT_MS = 120_000;
39
+ const RETRY_INTERVAL_MS = 60_000;
40
+ const USER_AGENT = `${SERVER_NAME}/${SERVER_VERSION}`;
41
+ function log(line) {
42
+ process.stderr.write(`[tickerbot-mcp] ${line}\n`);
43
+ }
44
+ function snapshotTools() {
45
+ return bakedTools.map((t) => ({
46
+ name: t.name,
47
+ description: t.description,
48
+ inputSchema: t.inputSchema,
49
+ }));
53
50
  }
54
51
  async function main() {
55
52
  const apiKey = process.env.TICKERBOT_API_KEY;
56
53
  if (!apiKey) {
57
- process.stderr.write(`[tickerbot-mcp] TICKERBOT_API_KEY is not set. Get a key from https://tickerbot.io/dashboard and add it to your MCP client config (env block).\n`);
54
+ log(`TICKERBOT_API_KEY is not set. Get a key from https://tickerbot.io/dashboard and add it to your MCP client config (env block).`);
55
+ }
56
+ const baseUrl = (process.env.TICKERBOT_API_URL || DEFAULT_BASE_URL).replace(/\/+$/, '');
57
+ const hostedUrl = `${baseUrl}/mcp`;
58
+ const hosted = apiKey
59
+ ? new HostedClient({ url: hostedUrl, apiKey, userAgent: USER_AGENT, clientVersion: SERVER_VERSION, clientLabel: `${SERVER_NAME}/${SERVER_VERSION}` })
60
+ : null;
61
+ // 'hosted': tools and calls go to {baseUrl}/mcp. 'snapshot': the bundled
62
+ // fallback, calls run here against the REST API.
63
+ let mode = 'snapshot';
64
+ let activeTools = snapshotTools();
65
+ let lastAttempt = 0;
66
+ async function tryHosted() {
67
+ if (!hosted)
68
+ return false;
69
+ lastAttempt = Date.now();
70
+ try {
71
+ activeTools = await hosted.connect(CONNECT_TIMEOUT_MS);
72
+ mode = 'hosted';
73
+ return true;
74
+ }
75
+ catch (err) {
76
+ const message = err instanceof Error ? err.message : String(err);
77
+ log(`WARNING: hosted MCP server unreachable (${message}). Serving the bundled snapshot of ${bakedTools.length} tools, which may be out of date; retrying at most once a minute.`);
78
+ return false;
79
+ }
58
80
  }
59
- const baseUrl = process.env.TICKERBOT_API_URL || DEFAULT_BASE_URL;
60
- // Fetch the live catalog before announcing tools. The handshake (`initialize`
61
- // + `tools/list`) happens after server.connect(), so we have a window to
62
- // populate `activeTools` with the server's current view.
63
- const live = await fetchLiveCatalog(baseUrl);
64
- const activeTools = live ?? bakedTools;
65
- const catalogSource = live ? 'live' : 'snapshot';
66
- const toolMap = new Map(activeTools.map((t) => [t.name, t]));
67
- const findTool = (name) => toolMap.get(name);
81
+ // Connect before announcing tools. The client's initialize + tools/list
82
+ // arrive after server.connect(), so they always see the result.
83
+ await tryHosted();
68
84
  const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
69
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
70
- tools: activeTools.map((t) => ({
71
- name: t.name,
72
- description: t.description,
73
- inputSchema: t.inputSchema,
74
- })),
75
- }));
85
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
86
+ if (mode === 'snapshot' && hosted && Date.now() - lastAttempt >= RETRY_INTERVAL_MS) {
87
+ if (await tryHosted())
88
+ log(`hosted MCP server reachable again; serving its ${activeTools.length} tools`);
89
+ }
90
+ return { tools: activeTools };
91
+ });
76
92
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
77
93
  const { name, arguments: args } = req.params;
94
+ if (mode === 'hosted' && hosted) {
95
+ try {
96
+ return await hosted.callTool(name, (args ?? {}), CALL_TIMEOUT_MS);
97
+ }
98
+ catch (err) {
99
+ if (err instanceof HostedRpcError)
100
+ throw new McpError(err.code, err.message, err.data);
101
+ // Not retried locally: the call may have reached the server (a POST
102
+ // that creates a webhook must not run twice).
103
+ const message = err instanceof Error ? err.message : String(err);
104
+ return {
105
+ content: [{ type: 'text', text: `Tickerbot MCP server unreachable: ${message}` }],
106
+ isError: true,
107
+ };
108
+ }
109
+ }
78
110
  try {
79
- const result = await runTool(name, (args ?? {}), apiKey ?? '', { baseUrl, findTool });
111
+ const result = await runTool(name, (args ?? {}), apiKey ?? '', {
112
+ baseUrl,
113
+ userAgent: USER_AGENT,
114
+ });
80
115
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
81
116
  }
82
117
  catch (err) {
@@ -93,7 +128,7 @@ async function main() {
93
128
  });
94
129
  const transport = new StdioServerTransport();
95
130
  await server.connect(transport);
96
- process.stderr.write(`[tickerbot-mcp] ${SERVER_NAME} v${SERVER_VERSION} ready (${activeTools.length} tools, catalog: ${catalogSource})\n`);
131
+ log(`${SERVER_NAME} v${SERVER_VERSION} ready (${activeTools.length} tools, catalog: ${mode === 'hosted' ? hostedUrl : 'bundled snapshot'})`);
97
132
  }
98
133
  function formatToolError(err) {
99
134
  const parts = [err.message];
@@ -108,6 +143,6 @@ function formatToolError(err) {
108
143
  return parts.join('\n');
109
144
  }
110
145
  main().catch((err) => {
111
- process.stderr.write(`[tickerbot-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
146
+ log(`fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
112
147
  process.exit(1);
113
148
  });
package/dist/tools.js CHANGED
@@ -3,11 +3,10 @@
3
3
  // GENERATED by scripts/regen-tools.mjs — do not edit by hand.
4
4
  //
5
5
  // The canonical tool list lives in main_service/mcp/tools.js on the
6
- // server. At startup, this package fetches `${baseUrl}/mcp/tools` and
7
- // uses the live catalog — see src/index.ts. This file is the snapshot
8
- // we fall back to if the fetch fails (offline, server down, network
9
- // blip), and it's also what gets bundled at npm-publish time for
10
- // freshness in cold-start scenarios.
6
+ // server. At startup this package connects to the hosted MCP server
7
+ // (`${baseUrl}/mcp`), serves its tools/list and forwards every call
8
+ // there — see src/index.ts (TB-740). This file is the snapshot it falls
9
+ // back to when that server cannot be reached, or no key is set.
11
10
  //
12
11
  // Staleness here is fine. Updating this file does NOT change behavior
13
12
  // for online users — they always see whatever main_service is serving.
@@ -209,6 +208,10 @@ export const tools = [
209
208
  "inputSchema": {
210
209
  "type": "object",
211
210
  "properties": {
211
+ "search": {
212
+ "type": "string",
213
+ "description": "Case-insensitive text match on the name and description."
214
+ },
212
215
  "kind": {
213
216
  "type": "string",
214
217
  "description": "Filter by kind (one or a comma list). Omit to return both.",
@@ -217,10 +220,6 @@ export const tools = [
217
220
  "custom"
218
221
  ]
219
222
  },
220
- "search": {
221
- "type": "string",
222
- "description": "Case-insensitive text match on the name and description."
223
- },
224
223
  "group": {
225
224
  "type": "string",
226
225
  "description": "One taxonomy group slug or a comma list (`technicals,fundamentals`). Groups and their categories are in `taxonomy`."
@@ -294,8 +293,8 @@ export const tools = [
294
293
  "method": "GET",
295
294
  "path": "/v2/signals",
296
295
  "paramLocation": {
297
- "kind": "query",
298
296
  "search": "query",
297
+ "kind": "query",
299
298
  "group": "query",
300
299
  "category": "query",
301
300
  "type": "query",
@@ -1471,6 +1470,10 @@ export const tools = [
1471
1470
  "inputSchema": {
1472
1471
  "type": "object",
1473
1472
  "properties": {
1473
+ "search": {
1474
+ "type": "string",
1475
+ "description": "Only universes whose id, name or description contains this text (case-insensitive)."
1476
+ },
1474
1477
  "kind": {
1475
1478
  "type": "string",
1476
1479
  "description": "Which universes to list: `builtin` (`top_10`, `top_100`), `custom` (your own) or both as a comma list. Leave it out for your own; from the cut-off on Deprecations it lists both.",
@@ -1479,10 +1482,6 @@ export const tools = [
1479
1482
  "custom"
1480
1483
  ]
1481
1484
  },
1482
- "search": {
1483
- "type": "string",
1484
- "description": "Only universes whose id, name or description contains this text (case-insensitive)."
1485
- },
1486
1485
  "limit": {
1487
1486
  "type": "integer",
1488
1487
  "description": "Page size. Max 100.",
@@ -1498,8 +1497,8 @@ export const tools = [
1498
1497
  "method": "GET",
1499
1498
  "path": "/v2/universes",
1500
1499
  "paramLocation": {
1501
- "kind": "query",
1502
1500
  "search": "query",
1501
+ "kind": "query",
1503
1502
  "limit": "query",
1504
1503
  "cursor": "query"
1505
1504
  },
@@ -1751,6 +1750,10 @@ export const tools = [
1751
1750
  "type": "string",
1752
1751
  "description": "Webhook id."
1753
1752
  },
1753
+ "search": {
1754
+ "type": "string",
1755
+ "description": "Only with news webhooks. The full-text search, as on Subscribe to news."
1756
+ },
1754
1757
  "kind": {
1755
1758
  "type": "string",
1756
1759
  "description": "Only with event webhooks. The kinds to fire on, as on Subscribe to events.",
@@ -1764,10 +1767,6 @@ export const tools = [
1764
1767
  "estimate_revision"
1765
1768
  ]
1766
1769
  },
1767
- "search": {
1768
- "type": "string",
1769
- "description": "Only with news webhooks. The full-text search, as on Subscribe to news."
1770
- },
1771
1770
  "condition": {
1772
1771
  "type": "string",
1773
1772
  "description": "Only with signal webhooks. The bound, in Get a signal's grammar (`<30`)."
@@ -1853,8 +1852,8 @@ export const tools = [
1853
1852
  "path": "/v2/webhooks/{webhook}",
1854
1853
  "paramLocation": {
1855
1854
  "webhook": "path",
1856
- "kind": "body",
1857
1855
  "search": "body",
1856
+ "kind": "body",
1858
1857
  "condition": "body",
1859
1858
  "ticker": "body",
1860
1859
  "universe": "body",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tickerbot/mcp-server",
3
3
  "mcpName": "io.github.tickerbot/mcp-server",
4
- "version": "0.3.1",
4
+ "version": "0.4.0",
5
5
  "description": "MCP server for Tickerbot — the stock market, in SQL. Scan, replay, or subscribe across ~12,000 US tickers and top 100 cryptos (338+ signal columns) from Claude, ChatGPT, Cursor, and other MCP clients.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -17,7 +17,8 @@
17
17
  "build": "tsc",
18
18
  "build:mcpb": "bash scripts/build-mcpb.sh",
19
19
  "prepublishOnly": "npm run build",
20
- "dev": "tsc --watch"
20
+ "dev": "tsc --watch",
21
+ "test": "npm run build && node --test test/*.test.mjs"
21
22
  },
22
23
  "engines": {
23
24
  "node": ">=18.0.0"