Jaspers Hub

jaspers / screener-mcp

pluginOfficialReviewed★ 0↓ 33

Jaspers Screener: Screens US SEC registrants by fundamentals, holders and insiders, then runs yes/no criteria against filings with cited quotes.

6 KB
1import { defineConnection, definePlugin, defineSource, defineView } from '@jaspers-ai/sdk'
2import { z } from 'zod'
3import { INSTRUCTIONS } from './instructions'
4import { ScreenerView } from './ScreenerView'
5import { DEFAULT_COLUMNS, DEFAULT_SORT, MAX_COLUMNS, MAX_CRITERIA, OPS, summarize, VERDICTS } from './screener'
6
7// The screener on the Jaspers screener MCP (screener-mcp), its own connection, screener-mcp/jaspers.
8// Five sources, one view. The schemas here are the contract with the orchestrator — what it may
9// set, and what it may read back — so the filters are the server's filter language exactly, in raw
10// units; which fields exist, and what each takes, is the server's to say (its guide), not ours.
11
12const Filter = z.object({
13 field: z.string().min(1),
14 op: z.enum(OPS),
15 /** between takes [low, high], in takes a list, is_null and not_null take none. Raw units. */
16 value: z.union([z.number(), z.string(), z.array(z.union([z.number(), z.string()]))]).optional(),
17})
18
19const Sort = z.object({ field: z.string().min(1), dir: z.enum(['asc', 'desc']) })
20
21/** A criterion of the qualitative stage: a question a reader of the filings can answer yes or no. The server's own shape. */
22const Criterion = z.object({
23 id: z.string().regex(/^[a-z0-9_]{1,40}$/),
24 question: z.string().min(5).max(500),
25})
26
27/**
28 * The panel's qualitative run. Setting `run` to {} with `criteria` in place is all the orchestrator
29 * does: the view starts the run over its list, follows it, and shows the verdicts. The fields are
30 * the view's own bookkeeping.
31 */
32const Run = z.object({
33 id: z.string().optional(),
34 status: z.enum(['running', 'done', 'error']).optional(),
35 error: z.string().optional(),
36})
37
38const StateSchema = z.object({
39 /** Always the COMPLETE set, joined by AND. */
40 filters: z.array(Filter).default([]),
41 /** The user's own list of tickers, as the SEC writes them; empty for every company. */
42 tickers: z.array(z.string()).max(1000).default([]),
43 includeUnlisted: z.boolean().default(false),
44 sort: Sort.default(DEFAULT_SORT),
45 page: z.number().int().min(0).default(0),
46 /** Fields shown besides the ones filtered and sorted on. */
47 columns: z.array(z.string()).max(MAX_COLUMNS).default(DEFAULT_COLUMNS),
48 /** The server's session. The view writes it; leave it alone. */
49 sessionId: z.string().nullable().default(null),
50 criteria: z.array(Criterion).max(MAX_CRITERIA).default([]),
51 run: Run.nullable().default(null),
52 /** Which verdicts the table shows once a run has results; empty for all. */
53 verdicts: z.array(z.enum(VERDICTS)).default([]),
54 /** The ticker whose evidence is open; empty for none. */
55 open: z.string().default(''),
56})
57
58const Progress = z.object({
59 total: z.number(),
60 queued: z.number(),
61 running: z.number(),
62 error: z.number(),
63 pass: z.number(),
64 fail: z.number(),
65 unclear: z.number(),
66 unverified: z.number(),
67})
68
69const OutputSchema = z.object({
70 session_id: z.string().nullable(),
71 universe: z.number(),
72 count: z.number(),
73 applied_filters: z.array(Filter.extend({ matches: z.number() })),
74 tickers: z.array(z.string()),
75 unknown_tickers: z.array(z.string()),
76 sort: Sort,
77 qualitative_ready: z.boolean(),
78 qualitative_max: z.number(),
79 qualitative: z
80 .object({
81 run_id: z.string().nullable(),
82 status: z.string(),
83 progress: Progress.nullable(),
84 passed: z.array(z.string()).optional(),
85 error: z.string().optional(),
86 })
87 .optional(),
88 open: z.string(),
89})
90
91// A call on a session is not a function of its arguments alone, since the session is on the server:
92// the rows of one call are never served to another.
93const screen = defineSource({
94 mcp: 'jaspers',
95 tool: 'screen_companies',
96 ttlMs: 0,
97 description:
98 'Screen US SEC registrants with filters {field, op, value} on any field of a company, narrowed over several calls in one session. Raw units: USD, fractions. With a screener element on screen, set its state instead.',
99})
100
101const guide = defineSource({
102 mcp: 'jaspers',
103 tool: 'get_guide',
104 ttlMs: 600_000,
105 description:
106 'How to read the screener\'s data. topic "fields": every field with its kind, unit and ops. "screening": the filter language with examples, and how the qualitative stage works. "overview": coverage, limits and freshness.',
107})
108
109const qualitativeStart = defineSource({
110 mcp: 'jaspers',
111 tool: 'start_qualitative_screen',
112 description:
113 'Start the qualitative stage over a session\'s list: every company read against criteria in words, with verified quotes. Costs model calls per company. With a screener element on screen, set its state.criteria and state.run instead; it starts and follows the run itself.',
114})
115
116const qualitative = defineSource({
117 mcp: 'jaspers',
118 tool: 'get_qualitative_screen',
119 ttlMs: 0,
120 description:
121 'Progress and results of a qualitative run. With evidence: true (and tickers or verdicts set) each criterion lists the ids of its citations, and citations holds each quote once with its title and link.',
122})
123
124const company = defineSource({
125 mcp: 'jaspers',
126 tool: 'get_company',
127 description: 'One company by ticker: identity, latest-fiscal-year fundamentals, institutional holders, insiders, newest filings.',
128})
129
130export default definePlugin({
131 id: 'screener-mcp',
132 connections: {
133 // The server signs its users in through Jaspers' Account: the terminal connects it on its own sign-in
134 // with Jaspers (before 0.4.0, on an OAuth flow of the connection's own, Authorize in Settings under
135 // Plugins) and sends the token as a bearer on every call. Nothing to paste, and nothing in the URL.
136 jaspers: defineConnection({
137 url: 'https://s.jsprai.com/mcp',
138 auth: 'oauth',
139 tools: ['screen_companies', 'start_qualitative_screen', 'get_qualitative_screen', 'get_company', 'get_guide'],
140 }),
141 },
142 sources: { screen, guide, 'qualitative-start': qualitativeStart, qualitative, company },
143 views: {
144 screener: defineView(ScreenerView, {
145 title: 'Screener',
146 state: StateSchema,
147 output: OutputSchema,
148 instructions: INSTRUCTIONS,
149 renders: [screen],
150 summarize: (state, output) => summarize(state, output),
151 }),
152 },
153})