@tickerbot/mcp-server 0.1.5 → 0.2.1

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/dist/tools.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // OFFLINE FALLBACK tool registry for the Tickerbot MCP server.
2
2
  //
3
+ // GENERATED by scripts/regen-tools.mjs — do not edit by hand.
4
+ //
3
5
  // The canonical tool list lives in main_service/mcp/tools.js on the
4
6
  // server. At startup, this package fetches `${baseUrl}/mcp/tools` and
5
7
  // uses the live catalog — see src/index.ts. This file is the snapshot
@@ -9,388 +11,1838 @@
9
11
  //
10
12
  // Staleness here is fine. Updating this file does NOT change behavior
11
13
  // for online users — they always see whatever main_service is serving.
12
- // Refresh this snapshot by running the regen script when the live
13
- // catalog has drifted enough that you want offline users to see the
14
- // newer version. No need to republish for every catalog tweak.
14
+ // Refresh with: node scripts/regen-tools.mjs (or --live)
15
15
  export const tools = [
16
- // ── Tickers ──────────────────────────────────────────────────────────
17
- {
18
- name: 'tickerbot_list_tickers',
19
- description: 'List active tickers from the Tickerbot universe (~12,000 US equities + top 100 crypto). Use `tickers` for bulk lookup of named symbols (returns full rows); otherwise walks the universe alphabetically with `cursor` pagination. Supports filters: search, asset_type, exchange, sector, min_market_cap.',
20
- inputSchema: {
21
- type: 'object',
22
- properties: {
23
- tickers: { type: 'string', description: 'Comma-separated symbols (max 50). When set, returns full rows for these symbols and pagination params are ignored.' },
24
- limit: { type: 'integer', description: 'Page size. Max 1000. Default 50.' },
25
- cursor: { type: 'string', description: 'Opaque cursor from a prior response.' },
26
- search: { type: 'string', description: 'Case-insensitive substring filter on ticker/name. Orders results by market_cap desc.' },
27
- asset_type: { type: 'string', description: 'Filter by asset type (e.g. "equity", "crypto").' },
28
- exchange: { type: 'string', description: 'Filter by exchange (e.g. "XNYS", "XNAS", "BATS").' },
29
- sector: { type: 'string', description: 'Exact-match sector filter (e.g. "Technology").' },
30
- min_market_cap: { type: 'number', description: 'Minimum market cap in USD. Orders results by market_cap desc.' },
16
+ {
17
+ "name": "tickerbot_list_tickers",
18
+ "description": "Every symbol we track, active or delisted, as one identity row each. Use `/v2/tickers/{ticker}` for the full row.",
19
+ "inputSchema": {
20
+ "type": "object",
21
+ "properties": {
22
+ "search": {
23
+ "type": "string",
24
+ "description": "Case-insensitive match on `ticker` or `name`, max 64 characters (longer is a 400). Results are ranked: an exact ticker match first, then symbols that start with the term, then name matches — alphabetical within each rank. The cursor carries the rank, so paging a search never repeats or skips."
25
+ },
26
+ "asset_class": {
27
+ "type": "string",
28
+ "description": "Filter by asset class — `stocks`, `rates`, `crypto`, `fx`, or a comma-separated list (the live classes today; validated for shape, not against a fixed list, so a well-formed class we don't track simply matches nothing — same contract as scan). Omit for every class. This is the class of INSTRUMENT, distinct from `asset_type` below (the instrument type within equities). Every row carries its `asset_class`, so a non-equity row identifies itself."
29
+ },
30
+ "asset_type": {
31
+ "type": "string",
32
+ "description": "Filter by instrument type WITHIN equities — the stored `asset_type` value (`CS`, `ETF`, `ADRC`, `PFD`, `FUND`, `UNIT`, `SP`, `ETS`, `WARRANT`, `RIGHT`, `ETN`, `ETV`), matched case-insensitively. `equity` is a convenience value expanding to the equity-like set. This is NOT an asset class: `asset_type=crypto` is rejected — use `asset_class=crypto`.",
33
+ "enum": [
34
+ "CS",
35
+ "ETF",
36
+ "ADRC",
37
+ "PFD",
38
+ "FUND",
39
+ "UNIT",
40
+ "SP",
41
+ "ETS",
42
+ "WARRANT",
43
+ "RIGHT",
44
+ "ETN",
45
+ "ETV",
46
+ "equity"
47
+ ]
48
+ },
49
+ "exchange": {
50
+ "type": "string",
51
+ "description": "Filter by exchange name — the value rows carry in their `exchange` field. MIC codes (`XNAS`, `XNYS`, `BATS`) are also accepted and match `exchange_mic`. A malformed value (non-letters, over 16 chars) is a 400.",
52
+ "enum": [
53
+ "NASDAQ",
54
+ "NYSE",
55
+ "NYSE Arca",
56
+ "Cboe BZX",
57
+ "NYSE American",
58
+ "OTC Link"
59
+ ]
60
+ },
61
+ "limit": {
62
+ "type": "integer",
63
+ "description": "Page size. Max 1000.",
64
+ "default": 50
65
+ },
66
+ "cursor": {
67
+ "type": "string",
68
+ "description": "Opaque cursor from the previous response's `next_cursor` field. Continues the walk from after that page. A cursor minted under `search` only resumes the same search."
69
+ }
70
+ }
71
+ },
72
+ "endpoint": {
73
+ "method": "GET",
74
+ "path": "/v2/tickers",
75
+ "paramLocation": {
76
+ "search": "query",
77
+ "asset_class": "query",
78
+ "asset_type": "query",
79
+ "exchange": "query",
80
+ "limit": "query",
81
+ "cursor": "query"
82
+ }
83
+ }
84
+ },
85
+ {
86
+ "name": "tickerbot_get_ticker",
87
+ "description": "The full ticker row, every signal on the schema page, for one symbol or a comma list of up to 50. Right now, or with `asof`, as of any past date. Pass a comma list of up to 50 symbols for a batch (`data` keyed by symbol plus `not_found`). Crypto is the X-prefixed pair (X:BTCUSD) — bare BTC/ETH are US-listed ETFs.",
88
+ "inputSchema": {
89
+ "type": "object",
90
+ "properties": {
91
+ "ticker": {
92
+ "type": "string",
93
+ "description": "One symbol, or a comma-separated list of up to 50 for a batch response keyed by symbol. Case-insensitive. Equities are bare symbols (`AAPL`); every other class carries a prefix — rates (`R:SOFR`), crypto (`X:BTCUSD`), fx (`X:EURUSD`). Bare `BTC`/`ETH` are US-listed ETFs, not spot crypto. See Tickers."
94
+ },
95
+ "asof": {
96
+ "type": "string",
97
+ "description": "Optional. Target moment as `YYYY-MM-DD` (that day's close) or an ISO timestamp (that intraday moment) — the same read as it stood then, unlimited depth. Full contract under As of a past date."
98
+ },
99
+ "interval": {
100
+ "type": "string",
101
+ "description": "Grain the past state is reconstructed at: `1m`, `1h`, `1d`, or `auto` (default). Only valid alongside `asof` — a live read with `interval` is a 400. Details under As of a past date.",
102
+ "enum": [
103
+ "1m",
104
+ "1h",
105
+ "1d",
106
+ "auto"
107
+ ],
108
+ "default": "auto"
109
+ }
31
110
  },
111
+ "required": [
112
+ "ticker"
113
+ ]
32
114
  },
33
- endpoint: {
34
- method: 'GET',
35
- path: '/v2/tickers',
36
- paramLocation: {
37
- tickers: 'query', limit: 'query', cursor: 'query', search: 'query',
38
- asset_type: 'query', exchange: 'query', sector: 'query', min_market_cap: 'query',
115
+ "endpoint": {
116
+ "method": "GET",
117
+ "path": "/v2/tickers/{ticker}",
118
+ "paramLocation": {
119
+ "ticker": "path",
120
+ "asof": "query",
121
+ "interval": "query"
122
+ }
123
+ }
124
+ },
125
+ {
126
+ "name": "tickerbot_subscribe_ticker",
127
+ "description": "Push one ticker: we POST your endpoint whenever it matches the condition you give. Webhooks need a paid plan (Free has no webhook slots). Omit `target_url` for in-app delivery.",
128
+ "inputSchema": {
129
+ "type": "object",
130
+ "properties": {
131
+ "ticker": {
132
+ "type": "string",
133
+ "description": "Case-insensitive. Equities are bare symbols (`AAPL`); every other class carries a prefix — rates (`R:SOFR`), crypto (`X:BTCUSD`), fx (`X:EURUSD`). Bare `BTC`/`ETH` are US-listed ETFs, not spot crypto. See Tickers."
134
+ },
135
+ "q": {
136
+ "type": "string",
137
+ "description": "WHERE-clause fragment using signal names from the schema — the same grammar as /v2/scan. (`condition` accepted as an alias.)"
138
+ },
139
+ "condition": {
140
+ "type": "string",
141
+ "description": "Original name for `q` — accepted as well. The same WHERE-clause fragment; send either spelling."
142
+ },
143
+ "target_url": {
144
+ "type": "string",
145
+ "description": "https:// URL to POST when the condition fires. Omit for in-app delivery (visible in the dashboard)."
146
+ },
147
+ "channel": {
148
+ "type": "string",
149
+ "description": "Delivery channel. `webhook` (POST to `target_url`), `discord` (post an embed to `discord_url`), `in_app` (dashboard only), or `mobile_push` (notify a phone signed in to the Tickerbot mobile app; requires a `device_id` from `POST /v2/devices/register`). Inferred when omitted: `webhook` if `target_url` is set, `discord` if `discord_url` is set, else `in_app`. `slack` is reserved and returns `501`. See the Delivery channels guide.",
150
+ "enum": [
151
+ "webhook",
152
+ "discord",
153
+ "in_app",
154
+ "mobile_push"
155
+ ]
156
+ },
157
+ "discord_url": {
158
+ "type": "string",
159
+ "description": "Discord incoming-webhook URL (`https://discord.com/api/webhooks/…`). Required when `channel` is `discord`. Stored as a posting credential: the create response echoes it back under `channel_config`, but every later read (list, get, deliveries) strips it and sets `channel_config_present: true` instead."
160
+ },
161
+ "device_id": {
162
+ "type": "string",
163
+ "description": "Device to notify, from `POST /v2/devices/register`. Required when `channel` is `mobile_push`; unknown ids are a 404 `device_not_found`."
164
+ },
165
+ "cadence": {
166
+ "type": "string",
167
+ "description": "How often to evaluate. `realtime` (the default) is evaluated on every data refresh (~1×/min); `hourly` and `nyse_open` throttle to a batch schedule. `1m` is a deprecated alias for `realtime`.",
168
+ "enum": [
169
+ "realtime",
170
+ "hourly",
171
+ "nyse_open"
172
+ ]
173
+ },
174
+ "name": {
175
+ "type": "string",
176
+ "description": "Human-readable label (up to 80 chars). Defaults to `<TICKER>: <query>`."
177
+ },
178
+ "columns": {
179
+ "type": "string",
180
+ "description": "Comma-separated extra signals to include in each fired payload match row, beyond the standard set (`ticker`, `name`, `asset_type`, `price`, `change_1d_pct`, `market_cap`). Each must be a real signal; an unknown signal is rejected at creation. `fields` accepted as an alias — and note the RESPONSE reports them under `fields`, as an array."
181
+ },
182
+ "order": {
183
+ "type": "string",
184
+ "description": "Signal the fired payload's match lists are sorted by before the 100-row cap is applied, so a truncated list is the deterministic top 100 rather than an arbitrary sample. Must be a real signal (validated at creation).",
185
+ "default": "market_cap"
186
+ },
187
+ "dir": {
188
+ "type": "string",
189
+ "description": "Sort direction for `order`.",
190
+ "enum": [
191
+ "asc",
192
+ "desc"
193
+ ],
194
+ "default": "desc"
195
+ }
39
196
  },
197
+ "required": [
198
+ "ticker",
199
+ "q"
200
+ ]
40
201
  },
202
+ "endpoint": {
203
+ "method": "POST",
204
+ "path": "/v2/tickers/{ticker}/subscribe",
205
+ "paramLocation": {
206
+ "ticker": "path",
207
+ "q": "body",
208
+ "condition": "body",
209
+ "target_url": "body",
210
+ "channel": "body",
211
+ "discord_url": "body",
212
+ "device_id": "body",
213
+ "cadence": "body",
214
+ "name": "body",
215
+ "columns": "body",
216
+ "order": "body",
217
+ "dir": "body"
218
+ }
219
+ }
41
220
  },
42
221
  {
43
- name: 'tickerbot_get_ticker',
44
- description: 'Get the full current row for one ticker — every column on the schema (price, change, indicators like rsi_14, every boolean flag like above_sma_50, fundamentals like pe_ratio). Pass `asof` (YYYY-MM-DD) for the row as it stood at the close of a past trading day.',
45
- inputSchema: {
46
- type: 'object',
47
- properties: {
48
- ticker: { type: 'string', description: 'Symbol. Case-insensitive. Equities: bare symbol (AAPL). Crypto: bare symbol (BTC).' },
49
- asof: { type: 'string', description: 'Optional YYYY-MM-DD. Returns the row at close of that day.' },
222
+ "name": "tickerbot_get_ticker_coverage",
223
+ "description": "For one ticker, what we hold and how far back — so an empty result is never ambiguous. Ask this before treating a gap in bars or series as an outage. `minute_tier.included: false` with `on_demand: true` is not a gap — sub-hour bars fetch from the provider on first request.",
224
+ "inputSchema": {
225
+ "type": "object",
226
+ "properties": {
227
+ "ticker": {
228
+ "type": "string",
229
+ "description": "Case-insensitive. Equities are bare symbols (`AAPL`); every other class carries a prefix — rates (`R:SOFR`), crypto (`X:BTCUSD`), fx (`X:EURUSD`). Bare `BTC`/`ETH` are US-listed ETFs, not spot crypto. See Tickers."
230
+ }
50
231
  },
51
- required: ['ticker'],
232
+ "required": [
233
+ "ticker"
234
+ ]
52
235
  },
53
- endpoint: {
54
- method: 'GET',
55
- path: '/v2/tickers/{ticker}',
56
- paramLocation: { ticker: 'path', asof: 'query' },
236
+ "endpoint": {
237
+ "method": "GET",
238
+ "path": "/v2/tickers/{ticker}/coverage",
239
+ "paramLocation": {
240
+ "ticker": "path"
241
+ }
242
+ }
243
+ },
244
+ {
245
+ "name": "tickerbot_list_signals",
246
+ "description": "Every signal you can name in a query — the built-in signals and your own custom signals, in one catalog. Use to discover the signal names and `q` vocabulary before composing a scan; custom signals appear with `kind: custom`.",
247
+ "inputSchema": {
248
+ "type": "object",
249
+ "properties": {
250
+ "kind": {
251
+ "type": "string",
252
+ "description": "Filter by kind (`expression` accepted as a legacy alias for `custom`). Omit to return both.",
253
+ "enum": [
254
+ "builtin",
255
+ "custom"
256
+ ]
257
+ },
258
+ "limit": {
259
+ "type": "integer",
260
+ "description": "Page size for the custom-signal slice. Max 200.",
261
+ "default": 50
262
+ },
263
+ "cursor": {
264
+ "type": "string",
265
+ "description": "Opaque cursor from a prior response."
266
+ }
267
+ }
57
268
  },
269
+ "endpoint": {
270
+ "method": "GET",
271
+ "path": "/v2/signals",
272
+ "paramLocation": {
273
+ "kind": "query",
274
+ "limit": "query",
275
+ "cursor": "query"
276
+ }
277
+ }
58
278
  },
59
279
  {
60
- name: 'tickerbot_get_ticker_history',
61
- description: 'Time-travel: get the full wide row for one ticker as it stood at a past date. Returns indicators, boolean flags, and the most-recent fundamentals known on that date. Use for backtests and reconstructions.',
62
- inputSchema: {
63
- type: 'object',
64
- properties: {
65
- ticker: { type: 'string', description: 'Symbol.' },
66
- asof: { type: 'string', description: 'Target date as YYYY-MM-DD or full ISO timestamp. Returns the most-recent daily snapshot on or before this date.' },
280
+ "name": "tickerbot_create_custom_signal",
281
+ "description": "A named boolean predicate you can reference anywhere a built-in signal goes.",
282
+ "inputSchema": {
283
+ "type": "object",
284
+ "properties": {
285
+ "name": {
286
+ "type": "string",
287
+ "description": "Slug — `^[a-z][a-z0-9_]{0,63}$`. Must not collide with any built-in signal name, and 15 names are reserved outright: `columns`, plus the `/v2/series` OHLCV aliases `open`/`high`/`low`/`close`/`volume`/`vwap`/`trades` and `o`/`h`/`l`/`c`/`v`/`vw`/`n` (those resolve to bars before custom lookup). This is the signal's API handle: it's what you reference in `q` and in the CRUD path."
288
+ },
289
+ "expr": {
290
+ "type": "string",
291
+ "description": "Boolean SQL predicate. May reference built-in signals and other custom signals you own. Must evaluate to true/false. Max 4000 chars. Stricter grammar than scan `q`: comparisons, `AND`/`OR`/`NOT`, `IN`, `BETWEEN`, `IS [NOT] NULL`, arithmetic, and the functions `abs`/`coalesce`/`round`/`least`/`greatest` only — no `LIKE`/`ILIKE`, no `CASE`, no `::` casts, no other functions. An expression that scans fine can still be rejected here with `compile_failed`. What you send is what you read back: responses echo your expression VERBATIM, not its expansion. A signal referencing another custom of yours returns the reference as you typed it — the inlined SQL exists only internally, and is what a subscribe endpoint freezes into a webhook."
292
+ },
293
+ "description": {
294
+ "type": "string",
295
+ "description": "Free-form notes. Max 500 chars. Absent or empty comes back as `\"\"` rather than null."
296
+ }
67
297
  },
68
- required: ['ticker', 'asof'],
298
+ "required": [
299
+ "name",
300
+ "expr"
301
+ ]
69
302
  },
70
- endpoint: {
71
- method: 'GET',
72
- path: '/v2/tickers/{ticker}/history',
73
- paramLocation: { ticker: 'path', asof: 'query' },
303
+ "endpoint": {
304
+ "method": "POST",
305
+ "path": "/v2/signals",
306
+ "paramLocation": {
307
+ "name": "body",
308
+ "expr": "body",
309
+ "description": "body"
310
+ }
311
+ }
312
+ },
313
+ {
314
+ "name": "tickerbot_update_custom_signal",
315
+ "description": "Update a custom signal you own — its expression, description, or name.",
316
+ "inputSchema": {
317
+ "type": "object",
318
+ "properties": {
319
+ "signal": {
320
+ "type": "string",
321
+ "description": "Custom signal slug (the signal name). A built-in name answers 404 — built-ins are read-only."
322
+ },
323
+ "expr": {
324
+ "type": "string",
325
+ "description": "New SQL expression. Re-validated and re-inlined against your other custom signals. Same strict grammar as create — no `LIKE`/`ILIKE`, `CASE`, `::` casts, or functions beyond `abs`/`coalesce`/`round`/`least`/`greatest`. The response echoes your expression verbatim, not its expansion."
326
+ },
327
+ "description": {
328
+ "type": "string",
329
+ "description": "New description. Not derived from `expr` — change both if the prose describes a threshold you are moving."
330
+ },
331
+ "new_name": {
332
+ "type": "string",
333
+ "description": "New slug — renames the signal and changes its API handle everywhere (same validation as create). Refused while other custom signals reference the current name. `name` is accepted as an alias (new_name wins when both are sent), but new_name is the unambiguous spelling since the URL already carries the current name."
334
+ }
335
+ },
336
+ "required": [
337
+ "signal"
338
+ ]
74
339
  },
340
+ "endpoint": {
341
+ "method": "PATCH",
342
+ "path": "/v2/signals/{signal}",
343
+ "paramLocation": {
344
+ "signal": "path",
345
+ "expr": "body",
346
+ "description": "body",
347
+ "new_name": "body"
348
+ }
349
+ }
75
350
  },
76
351
  {
77
- name: 'tickerbot_get_ticker_events',
78
- description: 'Get the discrete event log for one ticker (splits, dividends, analyst rating changes), newest-first. Optional date window and event-kind filter.',
79
- inputSchema: {
80
- type: 'object',
81
- properties: {
82
- ticker: { type: 'string', description: 'Symbol.' },
83
- from: { type: 'string', description: 'Earliest event timestamp (inclusive). YYYY-MM-DD or ISO.' },
84
- to: { type: 'string', description: 'Latest event timestamp (inclusive). YYYY-MM-DD or ISO.' },
85
- kind: { type: 'string', description: 'Filter by event kind.', enum: ['split', 'dividend', 'rating_change'] },
86
- limit: { type: 'integer', description: 'Page size. Max 1000. Default 100.' },
87
- cursor: { type: 'string', description: 'Opaque cursor.' },
352
+ "name": "tickerbot_delete_custom_signal",
353
+ "description": "Delete one of your custom signals. Refused by default if another of your signals references it.",
354
+ "inputSchema": {
355
+ "type": "object",
356
+ "properties": {
357
+ "signal": {
358
+ "type": "string",
359
+ "description": "Custom signal slug (the signal name). A built-in name answers 404 — built-ins are read-only."
360
+ },
361
+ "force": {
362
+ "type": "boolean",
363
+ "description": "When `true`, skip the reference check and delete. References will break on next recompile.",
364
+ "default": false
365
+ }
88
366
  },
89
- required: ['ticker'],
367
+ "required": [
368
+ "signal"
369
+ ]
90
370
  },
91
- endpoint: {
92
- method: 'GET',
93
- path: '/v2/tickers/{ticker}/events',
94
- paramLocation: { ticker: 'path', from: 'query', to: 'query', kind: 'query', limit: 'query', cursor: 'query' },
371
+ "endpoint": {
372
+ "method": "DELETE",
373
+ "path": "/v2/signals/{signal}",
374
+ "paramLocation": {
375
+ "signal": "path",
376
+ "force": "query"
377
+ }
378
+ }
379
+ },
380
+ {
381
+ "name": "tickerbot_get_signal",
382
+ "description": "The state of a signal is the set of tickers matching it right now, or with `asof`, as of any moment. One name, the whole market, one call. Booleans need no `condition`; numerics need one like \">70\". Sorted by signal value desc for numerics.",
383
+ "inputSchema": {
384
+ "type": "object",
385
+ "properties": {
386
+ "signal": {
387
+ "type": "string",
388
+ "description": "A signal name. Booleans (e.g. `golden_cross`, `above_sma_50`) are detected automatically; numerics (e.g. `rsi_14`, `market_cap`, `pe_ratio`) require a condition."
389
+ },
390
+ "asof": {
391
+ "type": "string",
392
+ "description": "Optional. Target moment as `YYYY-MM-DD` (that day's close) or an ISO timestamp (that intraday moment) — the same read as it stood then, unlimited depth. Full contract under As of a past date."
393
+ },
394
+ "interval": {
395
+ "type": "string",
396
+ "description": "Grain the past state is reconstructed at: `1m`, `1h`, `1d`, or `auto` (default). Only valid alongside `asof` — a live read with `interval` is a 400. Details under As of a past date.",
397
+ "enum": [
398
+ "1m",
399
+ "1h",
400
+ "1d",
401
+ "auto"
402
+ ],
403
+ "default": "auto"
404
+ },
405
+ "condition": {
406
+ "type": "string",
407
+ "description": "Required for numeric signals. Single bound, format `<op><value>`. Operators: `>`, `>=`, `=`, `!=`, `<`, `<=`. Examples: `>70`, `<=200`, `!=0`. Sending one with a boolean or custom signal returns 400 (it does not apply)."
408
+ },
409
+ "universe": {
410
+ "type": "string",
411
+ "description": "Optional. Scope to a system or caller-owned universe slug."
412
+ },
413
+ "limit": {
414
+ "type": "integer",
415
+ "description": "Page size. Max 200.",
416
+ "default": 50
417
+ },
418
+ "cursor": {
419
+ "type": "string",
420
+ "description": "Opaque cursor from the previous response."
421
+ },
422
+ "sort_by": {
423
+ "type": "string",
424
+ "description": "Row order: `default` (alphabetic for booleans, highest-value-first for numerics) or `market_cap` (desc NULLS LAST; adds `market_cap` to each row). Live only — with `asof` it is a 400 (the snapshot's order is fixed).",
425
+ "enum": [
426
+ "default",
427
+ "market_cap"
428
+ ],
429
+ "default": "default"
430
+ },
431
+ "include_active_since": {
432
+ "type": "boolean",
433
+ "description": "Built-in booleans only: adds `active_since` and `days_live` per row — the first day of the current true streak, from daily state (the day after the last false day; if the boolean has never been false since it first computed, the first true day). Looks back five years, so a boolean true for longer reports the window edge as a lower bound. Live only — a 400 with `asof`.",
434
+ "default": false
435
+ }
436
+ },
437
+ "required": [
438
+ "signal"
439
+ ]
95
440
  },
441
+ "endpoint": {
442
+ "method": "GET",
443
+ "path": "/v2/signals/{signal}",
444
+ "paramLocation": {
445
+ "signal": "path",
446
+ "asof": "query",
447
+ "interval": "query",
448
+ "condition": "query",
449
+ "universe": "query",
450
+ "limit": "query",
451
+ "cursor": "query",
452
+ "sort_by": "query",
453
+ "include_active_since": "query"
454
+ }
455
+ }
96
456
  },
97
- // ── Signals catalog ──────────────────────────────────────────────────
98
- {
99
- name: 'tickerbot_list_signals_catalog',
100
- description: 'List the unified signal catalog: every built-in column on the schema (`kind: builtin`) plus the caller\'s custom signals (`kind: expression`). Use to discover what `q=` clauses and signal names are available before composing a scan.',
101
- inputSchema: {
102
- type: 'object',
103
- properties: {
104
- kind: { type: 'string', description: 'Filter by kind. Omit for both.', enum: ['builtin', 'custom'] },
105
- limit: { type: 'integer', description: 'Page size for custom slice. Max 200. Default 50.' },
106
- cursor: { type: 'string', description: 'Opaque cursor.' },
457
+ {
458
+ "name": "tickerbot_subscribe_signal",
459
+ "description": "Push one signal: we POST your endpoint whenever any ticker starts matching it. Webhooks need a paid plan (Free has no webhook slots). Omit `ticker` to watch the whole universe.",
460
+ "inputSchema": {
461
+ "type": "object",
462
+ "properties": {
463
+ "signal": {
464
+ "type": "string",
465
+ "description": "Signal name from the schema (case-insensitive)."
466
+ },
467
+ "condition": {
468
+ "type": "string",
469
+ "description": "Required for numeric signals. Shape: `\">70\"`, `\"<30\"`, `\">=100\"`, `\"=50\"`. Sending one with a boolean or custom signal returns 400 (it does not apply)."
470
+ },
471
+ "ticker": {
472
+ "type": "string",
473
+ "description": "Restrict to a single ticker. Default: any ticker."
474
+ },
475
+ "universe": {
476
+ "type": "string",
477
+ "description": "Restrict to a system or user-owned universe (e.g. `top_100`). `universe_id` accepted as an alias. Unknown universes are a 404 `universe_not_found`."
478
+ },
479
+ "target_url": {
480
+ "type": "string",
481
+ "description": "https:// URL to POST when fired. Omit for in-app delivery."
482
+ },
483
+ "channel": {
484
+ "type": "string",
485
+ "description": "Delivery channel. `webhook` (POST to `target_url`), `discord` (post an embed to `discord_url`), `in_app` (dashboard only), or `mobile_push` (notify a phone signed in to the Tickerbot mobile app; requires a `device_id` from `POST /v2/devices/register`). Inferred when omitted: `webhook` if `target_url` is set, `discord` if `discord_url` is set, else `in_app`. `slack` is reserved and returns `501`. See the Delivery channels guide.",
486
+ "enum": [
487
+ "webhook",
488
+ "discord",
489
+ "in_app",
490
+ "mobile_push"
491
+ ]
492
+ },
493
+ "discord_url": {
494
+ "type": "string",
495
+ "description": "Discord incoming-webhook URL (`https://discord.com/api/webhooks/…`). Required when `channel` is `discord`. Stored as a posting credential: the create response echoes it back under `channel_config`, but every later read (list, get, deliveries) strips it and sets `channel_config_present: true` instead."
496
+ },
497
+ "device_id": {
498
+ "type": "string",
499
+ "description": "Device to notify, from `POST /v2/devices/register`. Required when `channel` is `mobile_push`; unknown ids are a 404 `device_not_found`."
500
+ },
501
+ "cadence": {
502
+ "type": "string",
503
+ "description": "`realtime` (the default) is evaluated on every data refresh (~1×/min); `hourly` and `nyse_open` throttle to a batch schedule. `1m` is a deprecated alias for `realtime`.",
504
+ "enum": [
505
+ "realtime",
506
+ "hourly",
507
+ "nyse_open"
508
+ ]
509
+ },
510
+ "name": {
511
+ "type": "string",
512
+ "description": "Human-readable label (up to 80 chars). Defaults to the predicate — `at_52w_high` for a boolean, `rsi_14 > 70` for a numeric, prefixed with `<TICKER>: ` when `ticker` scopes it."
513
+ },
514
+ "columns": {
515
+ "type": "string",
516
+ "description": "Comma-separated extra signals to include in each fired payload match row, beyond the standard set (`ticker`, `name`, `asset_type`, `price`, `change_1d_pct`, `market_cap`). Each must be a real signal; an unknown signal is rejected at creation. `fields` accepted as an alias — and note the RESPONSE reports them under `fields`, as an array."
517
+ },
518
+ "order": {
519
+ "type": "string",
520
+ "description": "Signal the fired payload's match lists are sorted by before the 100-row cap is applied, so a truncated list is the deterministic top 100 rather than an arbitrary sample. Must be a real signal (validated at creation).",
521
+ "default": "market_cap"
522
+ },
523
+ "dir": {
524
+ "type": "string",
525
+ "description": "Sort direction for `order`.",
526
+ "enum": [
527
+ "asc",
528
+ "desc"
529
+ ],
530
+ "default": "desc"
531
+ }
107
532
  },
533
+ "required": [
534
+ "signal"
535
+ ]
108
536
  },
109
- endpoint: {
110
- method: 'GET',
111
- path: '/v2/signals',
112
- paramLocation: { kind: 'query', limit: 'query', cursor: 'query' },
537
+ "endpoint": {
538
+ "method": "POST",
539
+ "path": "/v2/signals/{signal}/subscribe",
540
+ "paramLocation": {
541
+ "signal": "path",
542
+ "condition": "body",
543
+ "ticker": "body",
544
+ "universe": "body",
545
+ "target_url": "body",
546
+ "channel": "body",
547
+ "discord_url": "body",
548
+ "device_id": "body",
549
+ "cadence": "body",
550
+ "name": "body",
551
+ "columns": "body",
552
+ "order": "body",
553
+ "dir": "body"
554
+ }
555
+ }
556
+ },
557
+ {
558
+ "name": "tickerbot_scan",
559
+ "description": "Every ticker matching a SQL WHERE clause. Right now, or with `asof`, as of any past date. The `q` grammar is a flat SQL WHERE over signal names: AND/OR/NOT, comparisons, numeric and string literals, custom signals by name. No JOIN or subqueries. With `group_by` the result is rollup rows, not tickers. Example: `gap_up AND market_cap < 2000000000 AND NOT earnings_this_week`.",
560
+ "inputSchema": {
561
+ "type": "object",
562
+ "properties": {
563
+ "q": {
564
+ "type": "string",
565
+ "description": "SQL WHERE expression. Max 4000 chars; semicolons, comments and write keywords are rejected. Your custom signals are valid here — each expands to its SQL at run time."
566
+ },
567
+ "asof": {
568
+ "type": "string",
569
+ "description": "Optional. Target moment as `YYYY-MM-DD` (that day's close) or an ISO timestamp (that intraday moment) — the same read as it stood then, unlimited depth. Full contract under As of a past date."
570
+ },
571
+ "interval": {
572
+ "type": "string",
573
+ "description": "Grain the past state is reconstructed at: `1m`, `1h`, `1d`, or `auto` (default). Only valid alongside `asof` — a live read with `interval` is a 400. Details under As of a past date.",
574
+ "enum": [
575
+ "1m",
576
+ "1h",
577
+ "1d",
578
+ "auto"
579
+ ],
580
+ "default": "auto"
581
+ },
582
+ "order": {
583
+ "type": "string",
584
+ "description": "Signal to sort by. In aggregate mode the default is the count alias `tickers` — or, with a custom `select`, the last item's alias — sorted NULLS LAST with the group keys as tiebreak.",
585
+ "default": "change_1d_pct"
586
+ },
587
+ "dir": {
588
+ "type": "string",
589
+ "description": "Sort direction.",
590
+ "enum": [
591
+ "asc",
592
+ "desc"
593
+ ],
594
+ "default": "desc"
595
+ },
596
+ "limit": {
597
+ "type": "integer",
598
+ "description": "Page size. Max 100. Aggregate mode does not paginate — it sets `truncated: true` when groups were cut, so sort with `order` to keep the ones you want.",
599
+ "default": 50
600
+ },
601
+ "cursor": {
602
+ "type": "string",
603
+ "description": "Opaque cursor from the previous response's `next_cursor`. Row mode only."
604
+ },
605
+ "columns": {
606
+ "type": "string",
607
+ "description": "Extra signals per row, ADDITIVE — the defaults are always present (ticker, name, asset_class, asset_type, price, change_1d_pct, gap_pct, relative_volume, market_cap). `fields` accepted as an alias."
608
+ },
609
+ "full": {
610
+ "type": "boolean",
611
+ "description": "Return every signal instead of the default set. Mutually exclusive with `columns` — passing both is a 400.",
612
+ "default": false
613
+ },
614
+ "universe": {
615
+ "type": "string",
616
+ "description": "Slug of a system universe (`top_10`, `top_100`) or one of your own. Omitted, the scan runs across all ~21,033 tracked tickers."
617
+ },
618
+ "asset_class": {
619
+ "type": "string",
620
+ "description": "One or more asset classes — slug or comma-separated list (`stocks`, `rates`, `crypto`, `fx`). Validated for shape, not against a fixed list, so a well-formed class we don't track simply matches nothing. Echoed in `query`."
621
+ },
622
+ "group_by": {
623
+ "type": "string",
624
+ "description": "AGGREGATE MODE: 1–6 group keys (signals, expressions, or one of your custom signals as a boolean key). Results become rollup rows. Name a key with `AS` to choose its JSON key (`market_cap > 1e11 AS mega`); an un-named expression is named for you rather than returned as `?column?`. Incompatible with `columns`/`full`/`cursor`; works with `asof`."
625
+ },
626
+ "select": {
627
+ "type": "string",
628
+ "description": "Aggregate output items (requires `group_by`). Default: the group keys + `COUNT(*) AS tickers`. Supports count/avg/sum/min/max/stddev/string_agg/bool_and/bool_or plus `FILTER (WHERE …)`, and your custom signals inside expressions. Alias with `AS`; a last item without one is a 400."
629
+ },
630
+ "having": {
631
+ "type": "string",
632
+ "description": "Filter the aggregate rows (requires `group_by`). Custom signals are valid here too."
633
+ }
634
+ },
635
+ "required": [
636
+ "q"
637
+ ]
113
638
  },
639
+ "endpoint": {
640
+ "method": "POST",
641
+ "path": "/v2/scan",
642
+ "paramLocation": {
643
+ "q": "body",
644
+ "asof": "body",
645
+ "interval": "body",
646
+ "order": "body",
647
+ "dir": "body",
648
+ "limit": "body",
649
+ "cursor": "body",
650
+ "columns": "body",
651
+ "full": "body",
652
+ "universe": "body",
653
+ "asset_class": "body",
654
+ "group_by": "body",
655
+ "select": "body",
656
+ "having": "body"
657
+ }
658
+ }
114
659
  },
115
660
  {
116
- name: 'tickerbot_get_signals_match',
117
- description: 'Find tickers that match a single signal right now (or at a past date with `asof`). Booleans need no condition (returns tickers where flag is true). Numerics need a `condition` like ">70" or "<=200". Sorted by signal value desc for numerics.',
118
- inputSchema: {
119
- type: 'object',
120
- properties: {
121
- signal: { type: 'string', description: 'Column name on ticker. E.g. golden_cross_today, above_sma_50, rsi_14, market_cap, pe_ratio.' },
122
- condition: { type: 'string', description: 'Required for numerics. Single bound: <op><value>, ops in (>, >=, =, !=, <, <=). E.g. ">70".' },
123
- asof: { type: 'string', description: 'Optional YYYY-MM-DD. Match against historical daily state.' },
124
- universe: { type: 'string', description: 'Optional universe slug.' },
125
- limit: { type: 'integer', description: 'Page size. Max 200. Default 50.' },
126
- cursor: { type: 'string', description: 'Opaque cursor.' },
661
+ "name": "tickerbot_subscribe_scan",
662
+ "description": "Push a whole query: we POST your endpoint every time the match set changes. Webhooks need a paid plan (Free has no webhook slots). Use for \"alert me when this happens\" requests.",
663
+ "inputSchema": {
664
+ "type": "object",
665
+ "properties": {
666
+ "q": {
667
+ "type": "string",
668
+ "description": "WHERE-clause expression using signal names — the same grammar and the same 4000-char cap as `POST /v2/scan`, so anything scannable is subscribable. Custom signals are expanded and frozen in at creation."
669
+ },
670
+ "universe": {
671
+ "type": "string",
672
+ "description": "System or user-owned universe to scope the scan. `universe_id` accepted as an alias. Unknown universes are a 404 `universe_not_found`."
673
+ },
674
+ "target_url": {
675
+ "type": "string",
676
+ "description": "https:// URL to POST when the match set changes. Omit for in-app delivery."
677
+ },
678
+ "channel": {
679
+ "type": "string",
680
+ "description": "Delivery channel. `webhook` (POST to `target_url`), `discord` (embed to `discord_url`), `in_app` (dashboard only), or `mobile_push` (requires a `device_id` from `POST /v2/devices/register`). Inferred when omitted: `webhook` if `target_url` is set, `discord` if `discord_url` is set, else `in_app`. `slack` is reserved and returns `501`.",
681
+ "enum": [
682
+ "webhook",
683
+ "discord",
684
+ "in_app",
685
+ "mobile_push"
686
+ ]
687
+ },
688
+ "discord_url": {
689
+ "type": "string",
690
+ "description": "Discord incoming-webhook URL. Required when `channel` is `discord`. Stored as a posting credential: the create response echoes it back under `channel_config`, but every later read (list, get, deliveries) strips it and sets `channel_config_present: true` instead."
691
+ },
692
+ "device_id": {
693
+ "type": "string",
694
+ "description": "Device to notify, from `POST /v2/devices/register`. Required when `channel` is `mobile_push`; unknown ids are a 404 `device_not_found`."
695
+ },
696
+ "cadence": {
697
+ "type": "string",
698
+ "description": "`realtime` (the default) is evaluated on every data refresh (~1×/min); `hourly` and `nyse_open` throttle to a batch schedule. `1m` is a deprecated alias for `realtime`.",
699
+ "enum": [
700
+ "realtime",
701
+ "hourly",
702
+ "nyse_open"
703
+ ]
704
+ },
705
+ "name": {
706
+ "type": "string",
707
+ "description": "Human-readable label (up to 80 chars). Defaults to `scan: <q>`."
708
+ },
709
+ "columns": {
710
+ "type": "string",
711
+ "description": "Extra signals per fired payload match row, beyond the standard set (`ticker`, `name`, `asset_type`, `price`, `change_1d_pct`, `market_cap`). Each must be a real signal; unknown ones are rejected at creation. `fields` accepted as an alias — and the RESPONSE reports them under `fields`, as an array."
712
+ },
713
+ "order": {
714
+ "type": "string",
715
+ "description": "Signal the payload's match lists are sorted by before the 100-row cap applies, so a truncated list is the deterministic top 100 rather than an arbitrary sample. Must be a real signal (validated at creation).",
716
+ "default": "market_cap"
717
+ },
718
+ "dir": {
719
+ "type": "string",
720
+ "description": "Sort direction for `order`.",
721
+ "enum": [
722
+ "asc",
723
+ "desc"
724
+ ],
725
+ "default": "desc"
726
+ }
127
727
  },
128
- required: ['signal'],
728
+ "required": [
729
+ "q"
730
+ ]
129
731
  },
130
- endpoint: {
131
- method: 'GET',
132
- path: '/v2/signals/{signal}',
133
- paramLocation: { signal: 'path', condition: 'query', asof: 'query', universe: 'query', limit: 'query', cursor: 'query' },
732
+ "endpoint": {
733
+ "method": "POST",
734
+ "path": "/v2/scan/subscribe",
735
+ "paramLocation": {
736
+ "q": "body",
737
+ "universe": "body",
738
+ "target_url": "body",
739
+ "channel": "body",
740
+ "discord_url": "body",
741
+ "device_id": "body",
742
+ "cadence": "body",
743
+ "name": "body",
744
+ "columns": "body",
745
+ "order": "body",
746
+ "dir": "body"
747
+ }
748
+ }
749
+ },
750
+ {
751
+ "name": "tickerbot_get_series",
752
+ "description": "Any signals for any tickers on one shared time grid — up to 50 tickers by 25 columns per call. One flat row per ticker per interval step, cursor-paged backward. `transitions_only: true` with boolean signals returns only the rows where a boolean flipped.",
753
+ "inputSchema": {
754
+ "type": "object",
755
+ "properties": {
756
+ "tickers": {
757
+ "type": "string",
758
+ "description": "Comma-separated symbols, up to 50 (POST accepts a JSON array). Exactly one of `tickers` or `ticker` is required; when both are passed, `ticker` wins — so sending both silently narrows the request to one symbol."
759
+ },
760
+ "ticker": {
761
+ "type": "string",
762
+ "description": "Single-symbol form — `/v2/series?ticker=AAPL` is ticker history in its canonical spelling. Exactly one of `ticker` or `tickers` is required."
763
+ },
764
+ "columns": {
765
+ "type": "string",
766
+ "description": "Up to 25 columns (POST accepts an array): OHLCV names, signals, and your custom signals, freely mixed. Omitted → the ticker-history default set (price, change_1d_pct, relative_volume, market_cap), intersected with what the interval carries. At `1q`, `columns` is required and quarterly-only. `fields` accepted as an alias."
767
+ },
768
+ "interval": {
769
+ "type": "string",
770
+ "description": "Grid granularity. `1w` resamples the daily tier weekly (Monday-keyed); `1q` is the fiscal-quarter grid.",
771
+ "enum": [
772
+ "1m",
773
+ "1h",
774
+ "1d",
775
+ "1w",
776
+ "1q"
777
+ ],
778
+ "default": "1d"
779
+ },
780
+ "from": {
781
+ "type": "string",
782
+ "description": "Earliest timestamp (inclusive), `YYYY-MM-DD` or ISO. Intraday requests default to a recent window (`1m`: 7 days, `1h`: 60 days) — the cursor keeps walking further back window-by-window, or pass `from` to widen it up front."
783
+ },
784
+ "to": {
785
+ "type": "string",
786
+ "description": "Latest timestamp (inclusive), `YYYY-MM-DD` or ISO."
787
+ },
788
+ "asof": {
789
+ "type": "string",
790
+ "description": "Point-in-time read: ONE row per ticker — the state at that instant — rather than a range. `YYYY-MM-DD` or a full ISO timestamp, the same meaning `asof` carries on `/v2/tickers`, `/v2/scan` and `/v2/signals`. Cannot be combined with `from`/`to` or `cursor` (400) — a point and a window are contradictory, and `limit` has no meaning under it. It also resolves WHICH COMPANY held the symbol at that instant: a ticker that changed hands returns the row of whoever traded it then, so `tickers=SHLD&asof=2010-06-30` returns Sears Holdings' price and `asof=2026-01-01` returns the Global X defence ETF. Returns the most recent row at or before the instant, so a date inside a trading gap gives the last row before it. At `interval=1q` the anchor is the date the quarter was REPORTED (earnings release / filing), not fiscal period end — you get the latest quarter that was public knowledge at the instant, with restatements after it excluded."
791
+ },
792
+ "limit": {
793
+ "type": "integer",
794
+ "description": "Grid steps per page (shared across tickers). Max 1000 — an over-cap `limit` is clamped to 1000 (house convention, `limit=10000` means \"max\"). Separately, tickers × limit may not exceed 25,000 rows per page — over THAT cap is an explicit 400.",
795
+ "default": 252
796
+ },
797
+ "cursor": {
798
+ "type": "string",
799
+ "description": "Opaque cursor from the previous response — every ticker pages backward in lockstep on the shared grid, no per-ticker gaps or duplicates."
800
+ },
801
+ "transitions_only": {
802
+ "type": "boolean",
803
+ "description": "Only rows where a boolean signal changed state. Accepted spellings: `true`/`1`/`yes` and `false`/`0`/`no` (case-insensitive) — anything else is a 400, never silently off. Requires at least one boolean signal (built-in boolean or custom signal); each returned row carries `transitions: {column: \"enter\"|\"exit\"}`, and `_meta` lists the driving columns. Strict truth: only literal `true` is \"on\", so `null → true` is an enter and `true → null` an exit (a backfill boundary reads as an edge). Edges need a prior observation — on the oldest page of a walk the first row has no predecessor and yields no edge. A flip is dated by the state table and does not move with the column list: one recorded on a non-trading carry row keeps that date, with any bar columns `null` on that row (no bar exists there)."
804
+ }
805
+ }
134
806
  },
807
+ "endpoint": {
808
+ "method": "GET",
809
+ "path": "/v2/series",
810
+ "paramLocation": {
811
+ "tickers": "query",
812
+ "ticker": "query",
813
+ "columns": "query",
814
+ "interval": "query",
815
+ "from": "query",
816
+ "to": "query",
817
+ "asof": "query",
818
+ "limit": "query",
819
+ "cursor": "query",
820
+ "transitions_only": "query"
821
+ }
822
+ }
135
823
  },
136
824
  {
137
- name: 'tickerbot_get_signal_history',
138
- description: 'Get the time series of one signal for one ticker at a chosen interval (1m, 1h, 1d, 1w). Use for charting a numeric signal\'s evolution or seeing when a boolean flag was on/off across time.',
139
- inputSchema: {
140
- type: 'object',
141
- properties: {
142
- signal: { type: 'string', description: 'Column name.' },
143
- ticker: { type: 'string', description: 'Symbol.' },
144
- interval: { type: 'string', description: 'Bar interval.', enum: ['1m', '1h', '1d', '1w'] },
145
- from: { type: 'string', description: 'Earliest bar timestamp. YYYY-MM-DD or ISO.' },
146
- to: { type: 'string', description: 'Latest bar timestamp. YYYY-MM-DD or ISO.' },
147
- limit: { type: 'integer', description: 'Page size.' },
148
- cursor: { type: 'string', description: 'Opaque cursor.' },
825
+ "name": "tickerbot_get_bars",
826
+ "description": "OHLCV bars from 1-second through monthly. The feed underneath the table.",
827
+ "inputSchema": {
828
+ "type": "object",
829
+ "properties": {
830
+ "ticker": {
831
+ "type": "string",
832
+ "description": "Ticker symbol, or a comma-separated list (up to 50) for a bulk response keyed by symbol."
833
+ },
834
+ "interval": {
835
+ "type": "string",
836
+ "description": "Bar interval. `2h`/`4h` roll up hourly bars; `1w`/`1mo` roll up daily bars into calendar weeks (Monday start) and months — the bar's `t` is the bucket start (UTC), and with `asof` the last bucket is the week/month to date.",
837
+ "enum": [
838
+ "1s",
839
+ "1m",
840
+ "5m",
841
+ "15m",
842
+ "30m",
843
+ "1h",
844
+ "2h",
845
+ "4h",
846
+ "1d",
847
+ "1w",
848
+ "1mo"
849
+ ]
850
+ },
851
+ "from": {
852
+ "type": "string",
853
+ "description": "Window start (inclusive): `YYYY-MM-DD`, ISO timestamp, or epoch-ms. Combines with `to` for an explicit window; page within it using `cursor`. Mutually exclusive with `asof` and `before` (400)."
854
+ },
855
+ "to": {
856
+ "type": "string",
857
+ "description": "Window end (inclusive): a bare `YYYY-MM-DD` means through the end of that day, same as series. Mutually exclusive with `asof` and `before` (400)."
858
+ },
859
+ "asof": {
860
+ "type": "string",
861
+ "description": "Point-in-time: the most recent bar whose period had closed at or before that moment. A bare `YYYY-MM-DD` means that day's close. A full timestamp means the last FINISHED bar — at 10:00 ET on a Wednesday the day's close has not happened, so `1d` returns Tuesday's bar. Returns one bar unless you also pass `limit`, which gives the last `limit` closed bars. Mutually exclusive with `before`/`cursor` (400). Unlimited depth."
862
+ },
863
+ "adjusted": {
864
+ "type": "boolean",
865
+ "description": "Default `true`: prices are split-adjusted — restated after each later split, as the tape is, so a series is continuous across a split. `false` returns the price as it printed that day (a name that later did a 1:10 reverse split reads `21.4` adjusted and `2.14` on the tape), which is what a broker fill or a chart from that time shows. Volume scales the other way. Un-adjusted on read from the splits table; the store is untouched."
866
+ },
867
+ "session": {
868
+ "type": "string",
869
+ "description": "Sub-hour intervals only. `all` (default) includes pre- and post-market bars. `regular` keeps bars whose start is in 09:30–16:00 ET (DST-aware). Why you might want it: the vendor buckets trades by SIP report time, and late-reported off-exchange (Form T) prints on thin names can land 20 min to hours late in a pre-market minute — a `$1.70` print at 08:13 ET on a `$3.85` stock. Daily high/low are untouched by those. `limit` counts after the filter; paging still works.",
870
+ "enum": [
871
+ "all",
872
+ "regular"
873
+ ],
874
+ "default": "all"
875
+ },
876
+ "limit": {
877
+ "type": "integer",
878
+ "description": "Most-recent N bars. Max 1000 — an over-cap value is clamped, not an error.",
879
+ "default": 100
880
+ },
881
+ "before": {
882
+ "type": "string",
883
+ "description": "Return the N bars ending strictly before this date/timestamp — back-paging. Mutually exclusive with `cursor` (they are the same control — a 400 when both are sent)."
884
+ },
885
+ "cursor": {
886
+ "type": "string",
887
+ "description": "Continuation token from a prior response's `next_cursor`; sugar for `before` (sending both is a 400; a blank `cursor=` counts as absent), and the way to page inside a `from`/`to` window."
888
+ }
149
889
  },
150
- required: ['signal', 'ticker', 'interval'],
890
+ "required": [
891
+ "ticker",
892
+ "interval"
893
+ ]
151
894
  },
152
- endpoint: {
153
- method: 'GET',
154
- path: '/v2/signals/{signal}/{ticker}/history/{interval}',
155
- paramLocation: { signal: 'path', ticker: 'path', interval: 'path', from: 'query', to: 'query', limit: 'query', cursor: 'query' },
895
+ "endpoint": {
896
+ "method": "GET",
897
+ "path": "/v2/bars/{ticker}/{interval}",
898
+ "paramLocation": {
899
+ "ticker": "path",
900
+ "interval": "path",
901
+ "from": "query",
902
+ "to": "query",
903
+ "asof": "query",
904
+ "adjusted": "query",
905
+ "session": "query",
906
+ "limit": "query",
907
+ "before": "query",
908
+ "cursor": "query"
909
+ }
910
+ }
911
+ },
912
+ {
913
+ "name": "tickerbot_list_events",
914
+ "description": "One timeline across every ticker: earnings, dividends, splits, insider filings, analyst actions, plus opt-in signal firings and news. Requires at least one bound: a ticker scope (ticker/tickers/universe), a time window (from/to), or firm/action — `q` alone is not a bound. `firm`/`action` match case-insensitively; a `q` payload match is case-sensitive. `join: state` attaches the ticker state as of each event.",
915
+ "inputSchema": {
916
+ "type": "object",
917
+ "properties": {
918
+ "kind": {
919
+ "type": "string",
920
+ "description": "Comma list of kinds. Omitted → the five corporate kinds; `signal` and `news` join only when named here.",
921
+ "enum": [
922
+ "earnings",
923
+ "dividend",
924
+ "split",
925
+ "insider",
926
+ "analyst",
927
+ "news",
928
+ "signal"
929
+ ]
930
+ },
931
+ "ticker": {
932
+ "type": "string",
933
+ "description": "Single-ticker filter. When both `ticker` and `tickers` are passed, `ticker` wins."
934
+ },
935
+ "tickers": {
936
+ "type": "string",
937
+ "description": "Comma list of tickers (max 50). Mutually exclusive with `universe`."
938
+ },
939
+ "universe": {
940
+ "type": "string",
941
+ "description": "Universe slug (`top_10`, `top_100`, or one of yours) to scope the stream. Mutually exclusive with `tickers`."
942
+ },
943
+ "firm": {
944
+ "type": "string",
945
+ "description": "Analyst-only structured filter — requires `kind=analyst` alone (`400` otherwise). Exact firm-name match on the ratings feed."
946
+ },
947
+ "action": {
948
+ "type": "string",
949
+ "description": "Analyst-only structured filter — requires `kind=analyst` alone. Same `action` vocabulary as Analyst actions.",
950
+ "enum": [
951
+ "upgrades",
952
+ "downgrades",
953
+ "initiates_coverage_on",
954
+ "maintains",
955
+ "reiterates",
956
+ "assumes",
957
+ "reinstates",
958
+ "suspends",
959
+ "terminates_coverage_on"
960
+ ]
961
+ },
962
+ "signal": {
963
+ "type": "string",
964
+ "description": "Signal-only filter — requires `kind=signal` alone (`400` otherwise). One built-in boolean signal; REQUIRED with `q` or `join=state` on that kind. See Signal firings."
965
+ },
966
+ "transition": {
967
+ "type": "string",
968
+ "description": "Signal-only filter — requires `kind=signal` alone. `enter` (false→true) or `exit` (true→false).",
969
+ "enum": [
970
+ "enter",
971
+ "exit"
972
+ ]
973
+ },
974
+ "from": {
975
+ "type": "string",
976
+ "description": "Events at/after this instant — strict ISO: `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM[:SS]Z`. A bare `YYYY-MM-DD` means from the start of that day. (`since` accepted as an alias.)"
977
+ },
978
+ "to": {
979
+ "type": "string",
980
+ "description": "Window end — same strict ISO subset. A bare `YYYY-MM-DD` means through the end of that day, matching bars/series/spans; a timestamp is exclusive (events strictly before it). (`until` accepted as an alias.)"
981
+ },
982
+ "q": {
983
+ "type": "string",
984
+ "description": "SQL WHERE over the projection — `ticker`, `ts`, `kind`, `payload` (plus ticker-state signals when `join=state`). When exactly ONE `kind` is named, that kind's payload fields are additionally first-class typed columns (`amount > 1`, `firm = 'Goldman Sachs'` — see each kind page for its list); multi-kind requests use `payload->>'…'`. Max 4000 chars. ANDs with the filter params."
985
+ },
986
+ "join": {
987
+ "type": "string",
988
+ "description": "Set to `state` to allow ticker-state signals in `q`/`select`/`group_by`/`having`, evaluated as of each event's timestamp (daily resolution).",
989
+ "enum": [
990
+ "state"
991
+ ]
992
+ },
993
+ "interval": {
994
+ "type": "string",
995
+ "description": "Grain the per-event state is reconstructed at, when `join=state`: `1m`, `1h`, `1d`, or `auto` (default). `auto` resolves to `1d` — the event set's tickers are not known before the query runs, and `1d` is the only tier covering the whole universe, so it is the only grain guaranteed to satisfy every event. An explicit `1m`/`1h` trades coverage for precision: events on tickers absent from that tier join to `null`. A referenced column the grain does not store is a `400`. Reported back as `_meta.state_interval`.",
996
+ "enum": [
997
+ "1m",
998
+ "1h",
999
+ "1d",
1000
+ "auto"
1001
+ ],
1002
+ "default": "auto"
1003
+ },
1004
+ "select": {
1005
+ "type": "string",
1006
+ "description": "Aggregate-mode output columns (requires `group_by`). Default: group keys + `COUNT(*) AS events`. Same naming rule as `group_by` — alias with `AS`, or take the name derived for you."
1007
+ },
1008
+ "group_by": {
1009
+ "type": "string",
1010
+ "description": "Comma list of rollup keys — switches the response to aggregate rows. Columns (`kind`, `ticker`), payload fields (`firm`, or the explicit `payload->>'firm'`), and expressions over them all roll up. Name a key with `AS` to choose its JSON key: `payload->>'firm' AS firm`. Un-named keys are named for you — a payload read takes its key (`payload->>'firm'` → `firm`), a function keeps the function's name (`lower(ticker)` → `lower`), and anything else falls back to `group_1`, `group_2`."
1011
+ },
1012
+ "having": {
1013
+ "type": "string",
1014
+ "description": "Post-aggregation filter. Requires `group_by`."
1015
+ },
1016
+ "order": {
1017
+ "type": "string",
1018
+ "description": "Aggregate-mode sort — a bare column name or an output name only (put expressions in `select` and sort by their alias). A group key's name works too, whether you aliased it or it was named for you: `group_by=payload->>'firm' AS firm&order=firm`. Default: `events`. (Row mode is always newest-first.)"
1019
+ },
1020
+ "dir": {
1021
+ "type": "string",
1022
+ "description": "Aggregate-mode sort direction.",
1023
+ "enum": [
1024
+ "asc",
1025
+ "desc"
1026
+ ],
1027
+ "default": "desc"
1028
+ },
1029
+ "limit": {
1030
+ "type": "integer",
1031
+ "description": "Page size (row modes) / max rollup rows (aggregate mode). Max 1000.",
1032
+ "default": 50
1033
+ },
1034
+ "cursor": {
1035
+ "type": "string",
1036
+ "description": "Opaque cursor from the previous response — carries the original filters (and `q` when short), so pass it alone. Not valid with `group_by`."
1037
+ }
1038
+ }
156
1039
  },
1040
+ "endpoint": {
1041
+ "method": "GET",
1042
+ "path": "/v2/events",
1043
+ "paramLocation": {
1044
+ "kind": "query",
1045
+ "ticker": "query",
1046
+ "tickers": "query",
1047
+ "universe": "query",
1048
+ "firm": "query",
1049
+ "action": "query",
1050
+ "signal": "query",
1051
+ "transition": "query",
1052
+ "from": "query",
1053
+ "to": "query",
1054
+ "q": "query",
1055
+ "join": "query",
1056
+ "interval": "query",
1057
+ "select": "query",
1058
+ "group_by": "query",
1059
+ "having": "query",
1060
+ "order": "query",
1061
+ "dir": "query",
1062
+ "limit": "query",
1063
+ "cursor": "query"
1064
+ }
1065
+ }
157
1066
  },
158
1067
  {
159
- name: 'tickerbot_list_signal_events',
160
- description: 'Get the list of discrete firings of a boolean signal for one ticker (each time the flag went true). Newest-first, paginated.',
161
- inputSchema: {
162
- type: 'object',
163
- properties: {
164
- signal: { type: 'string', description: 'Boolean signal column.' },
165
- ticker: { type: 'string', description: 'Symbol.' },
166
- from: { type: 'string', description: 'Earliest event timestamp.' },
167
- to: { type: 'string', description: 'Latest event timestamp.' },
168
- limit: { type: 'integer', description: 'Page size.' },
169
- cursor: { type: 'string', description: 'Opaque cursor.' },
1068
+ "name": "tickerbot_subscribe_events",
1069
+ "description": "Push new events: we POST your endpoint when events of the kinds you chose land in the archives. Webhooks need a paid plan (Free has no webhook slots). `q` filters the ticker STATE; `event_q` filters the EVENT payload in the /v2/events grammar. Latency is the ingest cadence (analyst ≤1h, corporate kinds daily), not sub-minute.",
1070
+ "inputSchema": {
1071
+ "type": "object",
1072
+ "properties": {
1073
+ "kinds": {
1074
+ "type": "string",
1075
+ "description": "Event kinds to fire on — array or comma list.",
1076
+ "enum": [
1077
+ "earnings",
1078
+ "dividend",
1079
+ "split",
1080
+ "insider",
1081
+ "analyst"
1082
+ ]
1083
+ },
1084
+ "tickers": {
1085
+ "type": "string",
1086
+ "description": "Scope to specific tickers (max 50). Mutually exclusive with `universe` — and with the singular alias `ticker` (sending both is a 400). Omit both for all tickers."
1087
+ },
1088
+ "ticker": {
1089
+ "type": "string",
1090
+ "description": "Single-symbol shorthand for `tickers`."
1091
+ },
1092
+ "universe": {
1093
+ "type": "string",
1094
+ "description": "Scope to a universe slug (`top_10`, `top_100`, or one of yours). `universe_id` accepted as an alias."
1095
+ },
1096
+ "q": {
1097
+ "type": "string",
1098
+ "description": "Optional row-STATE filter evaluated against the event's ticker at fire time. Same grammar as scan `q`; custom signals are expanded and frozen at creation."
1099
+ },
1100
+ "event_q": {
1101
+ "type": "string",
1102
+ "description": "Optional event-CONTENT filter in the `/v2/events` grammar — only `ticker`, `ts`, `kind`, `payload` may appear. Composes with `q`."
1103
+ },
1104
+ "target_url": {
1105
+ "type": "string",
1106
+ "description": "HTTPS delivery URL; or use `channel` + `discord_url`/`device_id`. Omit for in-app."
1107
+ },
1108
+ "channel": {
1109
+ "type": "string",
1110
+ "description": "Delivery channel. `slack` is reserved and returns `501`.",
1111
+ "enum": [
1112
+ "webhook",
1113
+ "discord",
1114
+ "in_app",
1115
+ "mobile_push"
1116
+ ]
1117
+ },
1118
+ "discord_url": {
1119
+ "type": "string",
1120
+ "description": "Discord incoming-webhook URL. Required when `channel` is `discord`. Stored as a posting credential: the create response echoes it back under `channel_config`, but every later read (list, get, deliveries) strips it and sets `channel_config_present: true` instead."
1121
+ },
1122
+ "device_id": {
1123
+ "type": "string",
1124
+ "description": "Device to notify, from `POST /v2/devices/register`. Required when `channel` is `mobile_push`; unknown ids are a 404 `device_not_found`."
1125
+ },
1126
+ "name": {
1127
+ "type": "string",
1128
+ "description": "Display name. Defaults to `events: <kinds> · <scope>`."
1129
+ }
170
1130
  },
171
- required: ['signal', 'ticker'],
1131
+ "required": [
1132
+ "kinds"
1133
+ ]
172
1134
  },
173
- endpoint: {
174
- method: 'GET',
175
- path: '/v2/signals/{signal}/{ticker}/events',
176
- paramLocation: { signal: 'path', ticker: 'path', from: 'query', to: 'query', limit: 'query', cursor: 'query' },
1135
+ "endpoint": {
1136
+ "method": "POST",
1137
+ "path": "/v2/events/subscribe",
1138
+ "paramLocation": {
1139
+ "kinds": "body",
1140
+ "tickers": "body",
1141
+ "ticker": "body",
1142
+ "universe": "body",
1143
+ "q": "body",
1144
+ "event_q": "body",
1145
+ "target_url": "body",
1146
+ "channel": "body",
1147
+ "discord_url": "body",
1148
+ "device_id": "body",
1149
+ "name": "body"
1150
+ }
1151
+ }
1152
+ },
1153
+ {
1154
+ "name": "tickerbot_search_news",
1155
+ "description": "SQL query over the news archive. Article rows, or rollups when you group them. Filter to a ticker with the `ticker` param, or in `q` via the auto-unnest alias `tk = 'NVDA'`. `search` is full-text over title and summary. Rollups with group_by/having return `truncated: true` instead of paging.",
1156
+ "inputSchema": {
1157
+ "type": "object",
1158
+ "properties": {
1159
+ "ticker": {
1160
+ "type": "string",
1161
+ "description": "Articles mentioning this symbol (ANDed with `q`)."
1162
+ },
1163
+ "tickers": {
1164
+ "type": "string",
1165
+ "description": "Comma list, up to 50 — articles mentioning ANY of them. Not combinable with `ticker` or `universe`."
1166
+ },
1167
+ "universe": {
1168
+ "type": "string",
1169
+ "description": "Universe slug — articles mentioning any member. Not combinable with `ticker`/`tickers`."
1170
+ },
1171
+ "from": {
1172
+ "type": "string",
1173
+ "description": "Earliest `time_published` (inclusive) — strict ISO: `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM[:SS]Z`. (`since` accepted as an alias.)"
1174
+ },
1175
+ "to": {
1176
+ "type": "string",
1177
+ "description": "Articles strictly before this instant — same strict ISO subset, matching `/v2/events`. (`until` accepted as an alias.)"
1178
+ },
1179
+ "search": {
1180
+ "type": "string",
1181
+ "description": "Full-text search over `title` + `summary` — websearch grammar: `apple earnings` (all words), `\"price target\"` (phrase), `chips OR semiconductors`, `-crypto` (negation). Max 200 chars. ANDs with `q` and the scoping params. Language-stemmed English."
1182
+ },
1183
+ "q": {
1184
+ "type": "string",
1185
+ "description": "WHERE clause over the news_article table. Max 4000 chars. Required UNLESS `search` or a scoping param (`ticker`/`tickers`/`universe`/`from`/`to`) is present — the simplest call needs no SQL. Queryable columns: `time_published`, `title`, `summary`, `source`, `source_domain`, `category`, `authors`, `topics`, `overall_sentiment_score`, `overall_sentiment_label`, `tickers`, `ticker_data`, `banner_image`, `url`, `id`, `created_at` — plus `tk`, the per-ticker UNNEST alias. Signal/state columns are not joinable here."
1186
+ },
1187
+ "order": {
1188
+ "type": "string",
1189
+ "description": "Sort — a bare column name or SELECT alias only (put expressions in `select` and order by their alias). Defaults to `time_published` (article rows) or `volume` (aggregate rows)."
1190
+ },
1191
+ "dir": {
1192
+ "type": "string",
1193
+ "description": "Sort direction.",
1194
+ "enum": [
1195
+ "asc",
1196
+ "desc"
1197
+ ],
1198
+ "default": "desc"
1199
+ },
1200
+ "limit": {
1201
+ "type": "integer",
1202
+ "description": "Page size. Max 1000.",
1203
+ "default": 50
1204
+ },
1205
+ "cursor": {
1206
+ "type": "string",
1207
+ "description": "Opaque pagination cursor from a prior response's `next_cursor`."
1208
+ },
1209
+ "group_by": {
1210
+ "type": "string",
1211
+ "description": "AGGREGATE MODE: comma-separated group keys, 1-6 (max 1000 chars). Switches the response to rollup rows. Use `tk` to roll up per ticker without writing the UNNEST. Name a key with `AS` to choose its JSON key; an un-named expression is named for you rather than returned as `?column?`."
1212
+ },
1213
+ "select": {
1214
+ "type": "string",
1215
+ "description": "Columns/expressions to return (max 2000 chars). Defaults to article columns (no `group_by`) or `<group_by cols>, COUNT(*) AS volume` (with `group_by`)."
1216
+ },
1217
+ "having": {
1218
+ "type": "string",
1219
+ "description": "HAVING clause on the aggregate (max 1000 chars). Requires `group_by`."
1220
+ }
1221
+ }
177
1222
  },
1223
+ "endpoint": {
1224
+ "method": "GET",
1225
+ "path": "/v2/news",
1226
+ "paramLocation": {
1227
+ "ticker": "query",
1228
+ "tickers": "query",
1229
+ "universe": "query",
1230
+ "from": "query",
1231
+ "to": "query",
1232
+ "search": "query",
1233
+ "q": "query",
1234
+ "order": "query",
1235
+ "dir": "query",
1236
+ "limit": "query",
1237
+ "cursor": "query",
1238
+ "group_by": "query",
1239
+ "select": "query",
1240
+ "having": "query"
1241
+ }
1242
+ }
178
1243
  },
179
- // ── Scan ─────────────────────────────────────────────────────────────
180
- {
181
- name: 'tickerbot_scan',
182
- description: 'Run a SQL WHERE clause against the live ticker universe (or against a past trading day with `asof`). Returns matching tickers sorted by chosen column. The `q` grammar is a flat WHERE: column names from the schema, AND/OR/NOT, comparison operators, numeric/string literals. No JOIN, GROUP BY, or subqueries. Example: `gap_up AND market_cap < 2000000000 AND NOT earnings_this_week`.',
183
- inputSchema: {
184
- type: 'object',
185
- properties: {
186
- q: { type: 'string', description: 'SQL WHERE expression. Max 4000 chars. Identifiers are bare column names from the schema (e.g. above_sma_50, rsi_14, market_cap). Numeric literals fully specified (no 1.5B shorthand).' },
187
- universe: { type: 'string', description: 'Optional universe slug (top_10, top_100, or your own).' },
188
- asof: { type: 'string', description: 'Optional YYYY-MM-DD. Run the WHERE against historical close-of-day snapshot for this date.' },
189
- order: { type: 'string', description: 'Sort column. Default day_change_pct.' },
190
- dir: { type: 'string', description: 'Sort direction.', enum: ['asc', 'desc'] },
191
- fields: { type: 'string', description: 'Comma-separated extra columns to include in each result row beyond the default set.' },
192
- limit: { type: 'integer', description: 'Page size. Max 100. Default 50.' },
193
- cursor: { type: 'string', description: 'Opaque cursor.' },
194
- },
195
- required: ['q'],
196
- },
197
- endpoint: {
198
- method: 'GET',
199
- path: '/v2/scan',
200
- paramLocation: {
201
- q: 'query', universe: 'query', asof: 'query',
202
- order: 'query', dir: 'query', fields: 'query',
203
- limit: 'query', cursor: 'query',
1244
+ {
1245
+ "name": "tickerbot_get_etf_holdings",
1246
+ "description": "Returns an ETF's constituents and their weights, heaviest first. When the ticker is not an ETF, `is_etf` is false and `holdings` is empty; `is_etf: true` with zero holdings means a real ETF whose holdings are not ingested yet. The reverse lookup (\"which ETFs hold NVDA\") is a scan filter on `etf_holders`, not this tool.",
1247
+ "inputSchema": {
1248
+ "type": "object",
1249
+ "properties": {
1250
+ "ticker": {
1251
+ "type": "string",
1252
+ "description": "ETF symbol. Case-insensitive."
1253
+ },
1254
+ "limit": {
1255
+ "type": "integer",
1256
+ "description": "Max holdings returned. Max 5000. When the cap cuts the list, the response sets `truncated: true` and `total` (the ETF's full holding count) — raise `limit` to at least `total` to get the full set, possible whenever `total` is within the 5000 cap (an over-cap `limit` is clamped to 5000, not an error). No `truncated` in the response means the list is complete.",
1257
+ "default": 500
1258
+ }
204
1259
  },
1260
+ "required": [
1261
+ "ticker"
1262
+ ]
205
1263
  },
1264
+ "endpoint": {
1265
+ "method": "GET",
1266
+ "path": "/v2/etf/{ticker}/holdings",
1267
+ "paramLocation": {
1268
+ "ticker": "path",
1269
+ "limit": "query"
1270
+ }
1271
+ }
206
1272
  },
207
- // ── Universes ────────────────────────────────────────────────────────
208
- {
209
- name: 'tickerbot_list_universes',
210
- description: 'List the caller\'s saved universes (named ticker sets to scope scans). Each universe has a slug, name, and ticker count.',
211
- inputSchema: {
212
- type: 'object',
213
- properties: {
214
- limit: { type: 'integer', description: 'Page size.' },
215
- cursor: { type: 'string', description: 'Opaque cursor.' },
1273
+ {
1274
+ "name": "tickerbot_get_etf_sectors",
1275
+ "description": "Returns an ETF's sector weights, heaviest first. Always complete, since sector breakdowns are small. When the ticker is not an ETF, `is_etf` is false and `sectors` is empty.",
1276
+ "inputSchema": {
1277
+ "type": "object",
1278
+ "properties": {
1279
+ "ticker": {
1280
+ "type": "string",
1281
+ "description": "ETF symbol. Case-insensitive."
1282
+ }
216
1283
  },
1284
+ "required": [
1285
+ "ticker"
1286
+ ]
217
1287
  },
218
- endpoint: {
219
- method: 'GET',
220
- path: '/v2/universes',
221
- paramLocation: { limit: 'query', cursor: 'query' },
1288
+ "endpoint": {
1289
+ "method": "GET",
1290
+ "path": "/v2/etf/{ticker}/sectors",
1291
+ "paramLocation": {
1292
+ "ticker": "path"
1293
+ }
1294
+ }
1295
+ },
1296
+ {
1297
+ "name": "tickerbot_list_universes",
1298
+ "description": "Every universe you can reference: your own named ticker lists and the built-in ones. `owner: system` lists the built-in universes (top_10, top_100); `all` lists both. Use a slug as `universe` on scan, signal, and subscribe tools.",
1299
+ "inputSchema": {
1300
+ "type": "object",
1301
+ "properties": {
1302
+ "owner": {
1303
+ "type": "string",
1304
+ "description": "Which universes to list: `me` (your own), `system` (built-ins), or `all` (both).",
1305
+ "enum": [
1306
+ "me",
1307
+ "system",
1308
+ "all"
1309
+ ],
1310
+ "default": "me"
1311
+ },
1312
+ "limit": {
1313
+ "type": "integer",
1314
+ "description": "Page size (applies to your own). Max 100.",
1315
+ "default": 50
1316
+ },
1317
+ "cursor": {
1318
+ "type": "string",
1319
+ "description": "Opaque cursor from the previous response."
1320
+ }
1321
+ }
222
1322
  },
1323
+ "endpoint": {
1324
+ "method": "GET",
1325
+ "path": "/v2/universes",
1326
+ "paramLocation": {
1327
+ "owner": "query",
1328
+ "limit": "query",
1329
+ "cursor": "query"
1330
+ }
1331
+ }
223
1332
  },
224
1333
  {
225
- name: 'tickerbot_get_universe',
226
- description: 'Get one universe by slug, including its ticker list.',
227
- inputSchema: {
228
- type: 'object',
229
- properties: {
230
- id: { type: 'string', description: 'Universe slug.' },
1334
+ "name": "tickerbot_create_universe",
1335
+ "description": "Create a named ticker list owned by your account.",
1336
+ "inputSchema": {
1337
+ "type": "object",
1338
+ "properties": {
1339
+ "name": {
1340
+ "type": "string",
1341
+ "description": "Human-readable label, up to 80 characters. Display-only — never used to reference the universe."
1342
+ },
1343
+ "tickers": {
1344
+ "type": "array",
1345
+ "items": {
1346
+ "type": "string"
1347
+ },
1348
+ "description": "Ticker symbols, up to 10,000. Validated against the active universe. `[]` is accepted — a shell universe you can fill later via PATCH."
1349
+ },
1350
+ "id": {
1351
+ "type": "string",
1352
+ "description": "Optional slug — becomes the universe's permanent handle everywhere (`?universe=`, subscribe `universe`, CRUD path). Pattern `^[a-z][a-z0-9_]{0,62}$` — starts with a lowercase letter, then lowercase letters/digits/underscore, 63 chars max; the value is trimmed and lowercased before validation. `top_10` and `top_100` are reserved for system universes and rejected with 400. Must be unique within your account. Generated (`u_…`) if omitted."
1353
+ },
1354
+ "description": {
1355
+ "type": "string",
1356
+ "description": "Free-form notes, up to 500 characters. Stored as `\"\"` when omitted."
1357
+ }
231
1358
  },
232
- required: ['id'],
233
- },
234
- endpoint: {
235
- method: 'GET',
236
- path: '/v2/universes/{id}',
237
- paramLocation: { id: 'path' },
1359
+ "required": [
1360
+ "name",
1361
+ "tickers"
1362
+ ]
238
1363
  },
1364
+ "endpoint": {
1365
+ "method": "POST",
1366
+ "path": "/v2/universes",
1367
+ "paramLocation": {
1368
+ "name": "body",
1369
+ "tickers": "body",
1370
+ "id": "body",
1371
+ "description": "body"
1372
+ }
1373
+ }
239
1374
  },
240
1375
  {
241
- name: 'tickerbot_create_universe',
242
- description: 'Create a new universe (named set of tickers) for scoping future scans. Returns the new universe with its assigned slug.',
243
- inputSchema: {
244
- type: 'object',
245
- properties: {
246
- name: { type: 'string', description: 'Human-readable name.' },
247
- tickers: { type: 'array', description: 'List of ticker symbols.', items: { type: 'string' } },
1376
+ "name": "tickerbot_get_universe",
1377
+ "description": "Returns the universe doc. Use `top_10`/`top_100` to fetch a system universe; any other slug must be one your account owns.",
1378
+ "inputSchema": {
1379
+ "type": "object",
1380
+ "properties": {
1381
+ "id": {
1382
+ "type": "string",
1383
+ "description": "Universe slug."
1384
+ }
248
1385
  },
249
- required: ['name', 'tickers'],
1386
+ "required": [
1387
+ "id"
1388
+ ]
250
1389
  },
251
- endpoint: {
252
- method: 'POST',
253
- path: '/v2/universes',
254
- paramLocation: { name: 'body', tickers: 'body' },
1390
+ "endpoint": {
1391
+ "method": "GET",
1392
+ "path": "/v2/universes/{id}",
1393
+ "paramLocation": {
1394
+ "id": "path"
1395
+ }
1396
+ }
1397
+ },
1398
+ {
1399
+ "name": "tickerbot_update_universe",
1400
+ "description": "Update one of your universes: its name, description, or members. `tickers` replaces the whole list; `add`/`remove` adjust it. System universes cannot be edited.",
1401
+ "inputSchema": {
1402
+ "type": "object",
1403
+ "properties": {
1404
+ "id": {
1405
+ "type": "string",
1406
+ "description": "Universe slug."
1407
+ },
1408
+ "name": {
1409
+ "type": "string",
1410
+ "description": "New label. Non-empty, max 80 characters."
1411
+ },
1412
+ "description": {
1413
+ "type": "string",
1414
+ "description": "New notes. Max 500 characters."
1415
+ },
1416
+ "tickers": {
1417
+ "type": "array",
1418
+ "items": {
1419
+ "type": "string"
1420
+ },
1421
+ "description": "Replace the full ticker list (up to 10,000; validated against the active universe). Does not combine with `add`/`remove` (400)."
1422
+ },
1423
+ "add": {
1424
+ "type": "array",
1425
+ "items": {
1426
+ "type": "string"
1427
+ },
1428
+ "description": "Add these tickers (deduplicated)."
1429
+ },
1430
+ "remove": {
1431
+ "type": "array",
1432
+ "items": {
1433
+ "type": "string"
1434
+ },
1435
+ "description": "Remove these tickers."
1436
+ }
1437
+ },
1438
+ "required": [
1439
+ "id"
1440
+ ]
255
1441
  },
1442
+ "endpoint": {
1443
+ "method": "PATCH",
1444
+ "path": "/v2/universes/{id}",
1445
+ "paramLocation": {
1446
+ "id": "path",
1447
+ "name": "body",
1448
+ "description": "body",
1449
+ "tickers": "body",
1450
+ "add": "body",
1451
+ "remove": "body"
1452
+ }
1453
+ }
256
1454
  },
257
- // ── Custom signals (write) ───────────────────────────────────────────
258
- {
259
- name: 'tickerbot_create_custom_signal',
260
- description: 'Save a SQL WHERE expression as a named custom signal the caller can reference by name in future scans. E.g. name="oversold_with_volume", expr="rsi_14 < 30 AND volume_ratio_20d > 2".',
261
- inputSchema: {
262
- type: 'object',
263
- properties: {
264
- name: { type: 'string', description: 'Snake_case identifier for the signal (lowercase letters, digits, underscore).' },
265
- expr: { type: 'string', description: 'SQL WHERE expression. Same grammar as scan `q`.' },
266
- description: { type: 'string', description: 'Optional human description.' },
1455
+ {
1456
+ "name": "tickerbot_delete_universe",
1457
+ "description": "Permanently delete one of your universes. Webhooks that reference the deleted universe fail on their next eval — clean those up first.",
1458
+ "inputSchema": {
1459
+ "type": "object",
1460
+ "properties": {
1461
+ "id": {
1462
+ "type": "string",
1463
+ "description": "Universe slug."
1464
+ },
1465
+ "force": {
1466
+ "type": "boolean",
1467
+ "description": "A universe still referenced by live webhooks refuses to delete with `409 universe_referenced`. Pass `force=true` to delete anyway — those webhooks will match nothing until re-pointed or deleted.",
1468
+ "default": false
1469
+ }
267
1470
  },
268
- required: ['name', 'expr'],
1471
+ "required": [
1472
+ "id"
1473
+ ]
269
1474
  },
270
- endpoint: {
271
- method: 'POST',
272
- path: '/v2/signals',
273
- paramLocation: { name: 'body', expr: 'body', description: 'body' },
1475
+ "endpoint": {
1476
+ "method": "DELETE",
1477
+ "path": "/v2/universes/{id}",
1478
+ "paramLocation": {
1479
+ "id": "path",
1480
+ "force": "query"
1481
+ }
1482
+ }
1483
+ },
1484
+ {
1485
+ "name": "tickerbot_list_webhooks",
1486
+ "description": "Every webhook subscription on this account, newest first.",
1487
+ "inputSchema": {
1488
+ "type": "object",
1489
+ "properties": {
1490
+ "status": {
1491
+ "type": "string",
1492
+ "description": "Filter by status: `active` or `disabled` — the only two states a webhook has (`disabled` covers both a user pause and the automatic disable after repeated delivery failures; `consecutive_failures`/`last_error` on each record say which). Omit for all.",
1493
+ "enum": [
1494
+ "active",
1495
+ "disabled"
1496
+ ]
1497
+ },
1498
+ "limit": {
1499
+ "type": "integer",
1500
+ "description": "Page size. Max 100.",
1501
+ "default": 50
1502
+ },
1503
+ "cursor": {
1504
+ "type": "string",
1505
+ "description": "Opaque cursor from the previous response."
1506
+ }
1507
+ }
274
1508
  },
1509
+ "endpoint": {
1510
+ "method": "GET",
1511
+ "path": "/v2/webhooks",
1512
+ "paramLocation": {
1513
+ "status": "query",
1514
+ "limit": "query",
1515
+ "cursor": "query"
1516
+ }
1517
+ }
275
1518
  },
276
- // ── News ─────────────────────────────────────────────────────────────
277
- {
278
- name: 'tickerbot_search_news',
279
- description: 'Search the news archive (back to 2015) with a SQL WHERE clause. Plan-gated: Scale+ required for the archive. Columns on news_article include `time_published`, `title`, `summary`, `source`, `source_domain`, `category`, `authors`, `topics`, `tickers` (array), `overall_sentiment_score`, `overall_sentiment_label`, `url`. To filter to one ticker use `\'NVDA\' = ANY(tickers)` or the auto-unnest alias `tk = \'NVDA\'`. Example: `q=tk=\'NVDA\' AND time_published >= NOW() - INTERVAL \'1 day\'`. Supports group_by + having for aggregation (e.g. count of articles per day).',
280
- inputSchema: {
281
- type: 'object',
282
- properties: {
283
- q: { type: 'string', description: 'SQL WHERE on news_article. Required.' },
284
- select: { type: 'string', description: 'Comma-separated columns to include. Defaults to a slim set.' },
285
- group_by: { type: 'string', description: 'Comma-separated columns for aggregation (e.g. `date_trunc(\'day\', time_published)`).' },
286
- having: { type: 'string', description: 'WHERE-style filter on aggregates. Requires group_by.' },
287
- order: { type: 'string', description: 'Sort column or SELECT alias. Default time_published (non-aggregate) or volume (aggregate).' },
288
- dir: { type: 'string', description: 'Sort direction.', enum: ['asc', 'desc'] },
289
- limit: { type: 'integer', description: 'Page size.' },
290
- cursor: { type: 'string', description: 'Opaque cursor.' },
291
- },
292
- required: ['q'],
293
- },
294
- endpoint: {
295
- method: 'GET',
296
- path: '/v2/news/scan',
297
- paramLocation: {
298
- q: 'query', select: 'query', group_by: 'query', having: 'query',
299
- order: 'query', dir: 'query', limit: 'query', cursor: 'query',
1519
+ {
1520
+ "name": "tickerbot_create_webhook",
1521
+ "description": "Canonical create: a webhook is a trigger plus a delivery. Trigger shapes: scan {type:\"scan\", q, universe?}; ticker {type:\"ticker\", ticker, condition}; signal {type:\"signal\", signal, ticker?, universe?, condition?}; event {type:\"event\", kinds, tickers?, universe?, event_q?}. The subscribe tools are flat sugar over exactly this. Webhooks need a paid plan (Free has no webhook slots).",
1522
+ "inputSchema": {
1523
+ "type": "object",
1524
+ "properties": {
1525
+ "trigger": {
1526
+ "type": "object",
1527
+ "description": "What fires the webhook. A discriminated object — `trigger.type` picks the shape, and the fields below belong inside it. Each shape is also available as a flat-params shortcut: `POST /v2/scan/subscribe`, `/v2/tickers/{t}/subscribe`, `/v2/signals/{s}/subscribe`, `/v2/events/subscribe`.",
1528
+ "properties": {
1529
+ "type": {
1530
+ "type": "string",
1531
+ "description": "Which trigger shape the rest of the object uses.",
1532
+ "enum": [
1533
+ "scan",
1534
+ "ticker",
1535
+ "signal",
1536
+ "event"
1537
+ ]
1538
+ },
1539
+ "q": {
1540
+ "type": "string",
1541
+ "description": "scan: required — the SQL WHERE any ticker must match to fire. ticker: required — WHERE fragment evaluated for that ticker (auto-scoped; don't add `ticker = …` yourself; `trigger.condition` accepted as an alias). event: optional row-STATE filter on the event's ticker at fire time (`market_cap > 1e10`)."
1542
+ },
1543
+ "signal": {
1544
+ "type": "string",
1545
+ "description": "signal: required — a built-in signal name (e.g. `rsi_14`) or one of your custom signals (custom SQL is expanded and frozen at creation)."
1546
+ },
1547
+ "condition": {
1548
+ "type": "string",
1549
+ "description": "signal: required for numeric signals — a single bound like `>70`; sending one with a boolean or custom signal returns 400 (it does not apply). ticker: accepted as the original alias of `trigger.q`."
1550
+ },
1551
+ "ticker": {
1552
+ "type": "string",
1553
+ "description": "ticker: required — the symbol to watch (e.g. `NVDA`). signal: optional — restrict the signal to one symbol (omit to watch the whole universe)."
1554
+ },
1555
+ "tickers": {
1556
+ "type": "string",
1557
+ "description": "event: optional symbol list, max 50 (e.g. `AAPL,NVDA`). Mutually exclusive with `trigger.universe`."
1558
+ },
1559
+ "kinds": {
1560
+ "type": "string",
1561
+ "description": "event: required — event kinds to fire on, array or comma list (e.g. `split,analyst`)."
1562
+ },
1563
+ "event_q": {
1564
+ "type": "string",
1565
+ "description": "event: optional event-CONTENT filter in the `/v2/events` grammar over `(ticker, ts, kind, payload)` — e.g. `payload->>'firm' = 'Goldman Sachs'`. Composes with `trigger.q`."
1566
+ },
1567
+ "universe": {
1568
+ "type": "string",
1569
+ "description": "scan / signal / event: optional universe slug (`top_10`, `top_100`, or one of yours) scoping which tickers can fire. Mutually exclusive with `trigger.tickers` on event."
1570
+ }
1571
+ },
1572
+ "required": [
1573
+ "type"
1574
+ ]
1575
+ },
1576
+ "target_url": {
1577
+ "type": "string",
1578
+ "description": "HTTPS delivery URL (the `webhook` channel), max 1024 characters. Omit for in-app delivery, or use `channel` + `discord_url`/`device_id` for other channels."
1579
+ },
1580
+ "channel": {
1581
+ "type": "string",
1582
+ "description": "Delivery channel. See Delivery channels.",
1583
+ "enum": [
1584
+ "webhook",
1585
+ "discord",
1586
+ "in_app",
1587
+ "mobile_push"
1588
+ ]
1589
+ },
1590
+ "discord_url": {
1591
+ "type": "string",
1592
+ "description": "Discord webhook URL (channel `discord`)."
1593
+ },
1594
+ "device_id": {
1595
+ "type": "string",
1596
+ "description": "Registered device id (channel `mobile_push`, see /v2/devices)."
1597
+ },
1598
+ "cadence": {
1599
+ "type": "string",
1600
+ "description": "Evaluation cadence — a user preference — never gated. Event triggers deliver on ingest — only `realtime` is accepted on them (400 otherwise).",
1601
+ "enum": [
1602
+ "realtime",
1603
+ "hourly",
1604
+ "nyse_open"
1605
+ ],
1606
+ "default": "realtime"
1607
+ },
1608
+ "name": {
1609
+ "type": "string",
1610
+ "description": "Display name, max 80 characters. Defaults to an auto-generated one from the trigger."
1611
+ },
1612
+ "columns": {
1613
+ "type": "string",
1614
+ "description": "Extra columns echoed in fired payloads' match rows (`fields` accepted as an alias). Not accepted on `event` triggers (400) — event deliveries carry the event payload, not state rows."
1615
+ },
1616
+ "order": {
1617
+ "type": "string",
1618
+ "description": "Signal the fired payload's match lists are sorted by before the 100-row cap is applied — so a truncated list is the deterministic top 100, not an arbitrary sample. Same contract as `POST /v2/scan`. Not accepted on `event` triggers (they deliver one event at a time).",
1619
+ "default": "market_cap"
1620
+ },
1621
+ "dir": {
1622
+ "type": "string",
1623
+ "description": "Sort direction for `order`. Not accepted on `event` triggers (400).",
1624
+ "enum": [
1625
+ "asc",
1626
+ "desc"
1627
+ ],
1628
+ "default": "desc"
1629
+ }
300
1630
  },
1631
+ "required": [
1632
+ "trigger"
1633
+ ]
301
1634
  },
1635
+ "endpoint": {
1636
+ "method": "POST",
1637
+ "path": "/v2/webhooks",
1638
+ "paramLocation": {
1639
+ "trigger": "body",
1640
+ "target_url": "body",
1641
+ "channel": "body",
1642
+ "discord_url": "body",
1643
+ "device_id": "body",
1644
+ "cadence": "body",
1645
+ "name": "body",
1646
+ "columns": "body",
1647
+ "order": "body",
1648
+ "dir": "body"
1649
+ }
1650
+ }
302
1651
  },
303
- // ── Webhooks / subscribe (alert provisioning) ───────────────────────
304
- // Subscribe tools let the caller wire a webhook end-to-end in chat:
305
- // pass `target_url` to deliver to Discord/Slack/their own server;
306
- // omit it for in-app delivery (visible in the Tickerbot dashboard).
307
- // `cadence` is plan-gated — Hobby caps at 5m, Pro+ at 1m.
308
- {
309
- name: 'tickerbot_subscribe_scan',
310
- description: 'Register a webhook that fires when matches for a scan query change. Pass `target_url` to deliver to your own URL (Discord, Slack, server endpoint); omit for in-app delivery in the dashboard. `cadence` is plan-gated (Hobby max 5m, Pro+ 1m). Use to satisfy "alert me when this happens" prompts.',
311
- inputSchema: {
312
- type: 'object',
313
- properties: {
314
- q: { type: 'string', description: 'SQL WHERE expression — same grammar as scan.' },
315
- name: { type: 'string', description: 'Human-readable label. Defaults to a truncated version of the query.' },
316
- universe: { type: 'string', description: 'Optional universe slug to scope the watch.' },
317
- target_url: { type: 'string', description: 'Optional https URL to POST matches to. Omit for in-app delivery.' },
318
- cadence: { type: 'string', description: 'Evaluation cadence.', enum: ['1m', '5m', '15m', 'hourly', 'nyse_open'] },
1652
+ {
1653
+ "name": "tickerbot_get_webhook",
1654
+ "description": "The current state of one webhook subscription.",
1655
+ "inputSchema": {
1656
+ "type": "object",
1657
+ "properties": {
1658
+ "id": {
1659
+ "type": "string",
1660
+ "description": "Webhook id returned by a subscribe endpoint (`POST /v2/tickers/{T}/subscribe`, etc.)."
1661
+ }
319
1662
  },
320
- required: ['q'],
321
- },
322
- endpoint: {
323
- method: 'POST',
324
- path: '/v2/scan/subscribe',
325
- paramLocation: { q: 'body', name: 'body', universe: 'body', target_url: 'body', cadence: 'body' },
1663
+ "required": [
1664
+ "id"
1665
+ ]
326
1666
  },
1667
+ "endpoint": {
1668
+ "method": "GET",
1669
+ "path": "/v2/webhooks/{id}",
1670
+ "paramLocation": {
1671
+ "id": "path"
1672
+ }
1673
+ }
327
1674
  },
328
1675
  {
329
- name: 'tickerbot_subscribe_signal',
330
- description: 'Register a webhook that fires when a signal turns true (booleans) or its value crosses a condition (numerics). Optional `ticker` restricts to one symbol; omit to watch the whole universe. Pass `target_url` for outbound delivery or omit for in-app.',
331
- inputSchema: {
332
- type: 'object',
333
- properties: {
334
- signal: { type: 'string', description: 'Column name (e.g. golden_cross_today, rsi_14).' },
335
- ticker: { type: 'string', description: 'Optional ticker to restrict the watch to one symbol.' },
336
- universe: { type: 'string', description: 'Optional universe slug.' },
337
- condition: { type: 'string', description: 'Required for numerics: single bound like ">70" or "<=200". Ignored for booleans.' },
338
- name: { type: 'string', description: 'Human-readable label.' },
339
- target_url: { type: 'string', description: 'Optional https URL for delivery; omit for in-app.' },
340
- cadence: { type: 'string', description: 'Evaluation cadence.', enum: ['1m', '5m', '15m', 'hourly', 'nyse_open'] },
1676
+ "name": "tickerbot_update_webhook",
1677
+ "description": "Edit a webhook in place — send only the fields you want to change. The trigger and channel are immutable — delete and re-create to change what fires or where it delivers. Unknown fields are a 400.",
1678
+ "inputSchema": {
1679
+ "type": "object",
1680
+ "properties": {
1681
+ "id": {
1682
+ "type": "string",
1683
+ "description": "Webhook id."
1684
+ },
1685
+ "name": {
1686
+ "type": "string",
1687
+ "description": "New display name. Non-empty, max 80 characters."
1688
+ },
1689
+ "cadence": {
1690
+ "type": "string",
1691
+ "description": "Evaluation cadence. A user preference — never gated. Event triggers deliver on ingest — only `realtime` is accepted on them (400 otherwise).",
1692
+ "enum": [
1693
+ "realtime",
1694
+ "hourly",
1695
+ "nyse_open"
1696
+ ]
1697
+ },
1698
+ "target_url": {
1699
+ "type": "string",
1700
+ "description": "New https:// delivery URL (webhook channel only — a Discord/mobile subscription 400s here). `null` or empty switches to in-app delivery; `status` is untouched — a disabled webhook stays disabled until `POST /v2/webhooks/{id}/enable` (the only path that re-checks your account's webhook cap)."
1701
+ },
1702
+ "enabled": {
1703
+ "type": "boolean",
1704
+ "description": "`false` disables the webhook (status → `disabled`). `true` is a no-op unless disabled, in which case use `POST /v2/webhooks/{id}/enable` instead."
1705
+ }
341
1706
  },
342
- required: ['signal'],
343
- },
344
- endpoint: {
345
- method: 'POST',
346
- path: '/v2/signals/{signal}/subscribe',
347
- paramLocation: { signal: 'path', ticker: 'body', universe: 'body', condition: 'body', name: 'body', target_url: 'body', cadence: 'body' },
1707
+ "required": [
1708
+ "id"
1709
+ ]
348
1710
  },
1711
+ "endpoint": {
1712
+ "method": "PATCH",
1713
+ "path": "/v2/webhooks/{id}",
1714
+ "paramLocation": {
1715
+ "id": "path",
1716
+ "name": "body",
1717
+ "cadence": "body",
1718
+ "target_url": "body",
1719
+ "enabled": "body"
1720
+ }
1721
+ }
349
1722
  },
350
1723
  {
351
- name: 'tickerbot_subscribe_ticker',
352
- description: 'Register a webhook that fires when one ticker matches a condition. `condition` is a SQL WHERE-clause fragment scoped to that ticker (e.g. "rsi_14 > 70 AND relative_volume > 2"). Pass `target_url` for outbound delivery or omit for in-app.',
353
- inputSchema: {
354
- type: 'object',
355
- properties: {
356
- ticker: { type: 'string', description: 'Symbol.' },
357
- condition: { type: 'string', description: 'SQL WHERE fragment evaluated for this ticker (e.g. "rsi_14 > 70 AND gap_up").' },
358
- name: { type: 'string', description: 'Human-readable label.' },
359
- target_url: { type: 'string', description: 'Optional https URL for delivery; omit for in-app.' },
360
- cadence: { type: 'string', description: 'Evaluation cadence.', enum: ['1m', '5m', '15m', 'hourly', 'nyse_open'] },
1724
+ "name": "tickerbot_delete_webhook",
1725
+ "description": "Delete a webhook subscription, and its delivery history with it.",
1726
+ "inputSchema": {
1727
+ "type": "object",
1728
+ "properties": {
1729
+ "id": {
1730
+ "type": "string",
1731
+ "description": "Webhook id."
1732
+ }
361
1733
  },
362
- required: ['ticker', 'condition'],
363
- },
364
- endpoint: {
365
- method: 'POST',
366
- path: '/v2/tickers/{ticker}/subscribe',
367
- paramLocation: { ticker: 'path', condition: 'body', name: 'body', target_url: 'body', cadence: 'body' },
1734
+ "required": [
1735
+ "id"
1736
+ ]
368
1737
  },
1738
+ "endpoint": {
1739
+ "method": "DELETE",
1740
+ "path": "/v2/webhooks/{id}",
1741
+ "paramLocation": {
1742
+ "id": "path"
1743
+ }
1744
+ }
369
1745
  },
370
1746
  {
371
- name: 'tickerbot_list_webhooks',
372
- description: 'List the caller\'s active webhook subscriptions (rules created via the subscribe tools).',
373
- inputSchema: {
374
- type: 'object',
375
- properties: {
376
- limit: { type: 'integer', description: 'Page size.' },
377
- cursor: { type: 'string', description: 'Opaque cursor.' },
1747
+ "name": "tickerbot_list_webhook_deliveries",
1748
+ "description": "Recent deliveries for one webhook — what was sent, and what came back.",
1749
+ "inputSchema": {
1750
+ "type": "object",
1751
+ "properties": {
1752
+ "id": {
1753
+ "type": "string",
1754
+ "description": "Webhook id."
1755
+ },
1756
+ "status": {
1757
+ "type": "string",
1758
+ "description": "Filter by delivery status.",
1759
+ "enum": [
1760
+ "pending",
1761
+ "delivered",
1762
+ "permanent_failure"
1763
+ ]
1764
+ },
1765
+ "from": {
1766
+ "type": "string",
1767
+ "description": "Only deliveries created at/after this moment — epoch seconds, epoch milliseconds (13+ digits), or an ISO datetime (`since` is accepted as an alias). Delivery history is retained for 90 days; deleting a webhook deletes its delivery history with it."
1768
+ },
1769
+ "to": {
1770
+ "type": "string",
1771
+ "description": "Only deliveries created at/before this moment — same value grammar as `from`. A date-only value means through the end of that UTC day. `from` after `to` is a 400."
1772
+ },
1773
+ "limit": {
1774
+ "type": "integer",
1775
+ "description": "Page size. Max 100.",
1776
+ "default": 50
1777
+ },
1778
+ "cursor": {
1779
+ "type": "string",
1780
+ "description": "Opaque cursor."
1781
+ }
378
1782
  },
1783
+ "required": [
1784
+ "id"
1785
+ ]
379
1786
  },
380
- endpoint: { method: 'GET', path: '/v2/webhooks', paramLocation: { limit: 'query', cursor: 'query' } },
1787
+ "endpoint": {
1788
+ "method": "GET",
1789
+ "path": "/v2/webhooks/{id}/deliveries",
1790
+ "paramLocation": {
1791
+ "id": "path",
1792
+ "status": "query",
1793
+ "from": "query",
1794
+ "to": "query",
1795
+ "limit": "query",
1796
+ "cursor": "query"
1797
+ }
1798
+ }
381
1799
  },
382
1800
  {
383
- name: 'tickerbot_delete_webhook',
384
- description: 'Delete a webhook subscription by id. Use after listing webhooks when the user wants to remove an alert.',
385
- inputSchema: {
386
- type: 'object',
387
- properties: {
388
- id: { type: 'string', description: 'Webhook id (looks like `wh_…`).' },
1801
+ "name": "tickerbot_enable_webhook",
1802
+ "description": "Re-enable a disabled webhook and start it clean. Clears match-state, so the next eval treats every currently-matching ticker as new.",
1803
+ "inputSchema": {
1804
+ "type": "object",
1805
+ "properties": {
1806
+ "id": {
1807
+ "type": "string",
1808
+ "description": "Webhook id."
1809
+ }
389
1810
  },
390
- required: ['id'],
1811
+ "required": [
1812
+ "id"
1813
+ ]
391
1814
  },
392
- endpoint: { method: 'DELETE', path: '/v2/webhooks/{id}', paramLocation: { id: 'path' } },
1815
+ "endpoint": {
1816
+ "method": "POST",
1817
+ "path": "/v2/webhooks/{id}/enable",
1818
+ "paramLocation": {
1819
+ "id": "path"
1820
+ }
1821
+ }
393
1822
  },
1823
+ {
1824
+ "name": "tickerbot_test_webhook",
1825
+ "description": "Send a real-shape test POST to your endpoint, instantly. One-shot: a failed test never retries and never auto-disables the webhook. 400 when the webhook has no target_url.",
1826
+ "inputSchema": {
1827
+ "type": "object",
1828
+ "properties": {
1829
+ "id": {
1830
+ "type": "string",
1831
+ "description": "Webhook id."
1832
+ }
1833
+ },
1834
+ "required": [
1835
+ "id"
1836
+ ]
1837
+ },
1838
+ "endpoint": {
1839
+ "method": "POST",
1840
+ "path": "/v2/webhooks/{id}/test",
1841
+ "paramLocation": {
1842
+ "id": "path"
1843
+ }
1844
+ }
1845
+ }
394
1846
  ];
395
1847
  export function findTool(name) {
396
1848
  return tools.find((t) => t.name === name);