Jaspers Hub

jaspers / earningscall

pluginOfficialReviewed★ 0↓ 6

EarningsCall Transcripts & Calendar: Shows a company's earnings call transcripts and the daily earnings calendar from EarningsCall.biz in a single panel view.

3.3 KB
1import { defineConnection, definePlugin, defineSource, defineView } from '@jaspers-ai/sdk'
2import { z } from 'zod'
3import { summarize } from './calls'
4import { CallsView } from './CallsView'
5import { CALLS_INSTRUCTIONS } from './instructions'
6
7// Earnings calls from EarningsCall.biz over its own connection, earningscall/server: one source per
8// tool the connection offers, and one view that shows a call's transcript or a day's calendar. The
9// sources carry no description of their own, so the orchestrator reads the server's. It gets
10// earningscall__calls, earningscall__transcript, and earningscall__calendar, and places the view
11// instead of calling them for what the view shows.
12
13const MINUTE = 60_000
14
15const calls = defineSource({ mcp: 'server', tool: 'list_calls', ttlMs: 15 * MINUTE })
16// A published transcript does not change.
17const transcript = defineSource({ mcp: 'server', tool: 'get_transcript', ttlMs: 60 * MINUTE })
18const calendar = defineSource({ mcp: 'server', tool: 'get_calendar', ttlMs: 15 * MINUTE })
19
20const Call = z.object({ year: z.number(), quarter: z.number(), date: z.string().nullable(), published: z.boolean() })
21
22const CallsState = z.object({
23 symbol: z.string().optional(),
24 exchange: z.string().optional(),
25 year: z.number().int().nullable().optional(),
26 quarter: z.number().int().min(1).max(4).nullable().optional(),
27 date: z
28 .string()
29 .regex(/^\d{4}-\d{2}-\d{2}$/)
30 .nullable()
31 .optional(),
32 find: z.string().optional(),
33})
34
35const CallsOutput = z.object({
36 mode: z.enum(['call', 'calendar']),
37 error: z.string().nullable(),
38 // A call.
39 symbol: z.string().optional(),
40 exchange: z.string().nullable().optional(),
41 company: z.string().nullable().optional(),
42 year: z.number().nullable().optional(),
43 quarter: z.number().nullable().optional(),
44 date: z.string().nullable().optional(),
45 calls: z.array(Call).optional(),
46 paragraphs: z.number().optional(),
47 qaParagraph: z.number().nullable().optional(),
48 find: z.string().optional(),
49 matches: z.number().optional(),
50 snippets: z.array(z.object({ paragraph: z.number(), text: z.string() })).optional(),
51 // A calendar day.
52 count: z.number().optional(),
53 events: z
54 .array(
55 z.object({
56 exchange: z.string(),
57 symbol: z.string(),
58 company: z.string(),
59 year: z.number(),
60 quarter: z.number(),
61 date: z.string().nullable(),
62 ready: z.boolean(),
63 }),
64 )
65 .optional(),
66})
67
68export default definePlugin({
69 id: 'earningscall',
70 secrets: { apikey: { label: 'EarningsCall API key' } },
71 connections: {
72 // EarningsCall.biz has a REST API and no MCP server, so this runs the small one in server/, on
73 // the Node the app runs on, with the key in its environment; the server keeps it out of what it
74 // says. Its transcripts are the plain kind every plan has.
75 server: defineConnection({
76 command: ['node', '--disable-warning=MODULE_TYPELESS_PACKAGE_JSON', 'server/main.ts'],
77 env: { EARNINGSCALL_API_KEY: '${secret:apikey}' },
78 tools: ['list_calls', 'get_transcript', 'get_calendar'],
79 }),
80 },
81 sources: { calls, transcript, calendar },
82 views: {
83 calls: defineView(CallsView, {
84 title: 'Earnings calls',
85 state: CallsState,
86 output: CallsOutput,
87 instructions: CALLS_INSTRUCTIONS,
88 renders: [calls, transcript, calendar],
89 summarize,
90 }),
91 },
92})