FMP: News & Technicals: Connects Financial Modeling Prep's MCP server to show company news and technical indicators, with Form 13F as a source only.
| 1 | import { defineConnection, definePlugin, defineSource, defineView } from '@jaspers-ai/sdk' |
| 2 | import { z } from 'zod' |
| 3 | import { KINDS, newsSummary, technicalsSummary, TIMEFRAMES } from './fmp' |
| 4 | import { NEWS_INSTRUCTIONS, TECHNICALS_INSTRUCTIONS } from './instructions' |
| 5 | import { NewsView } from './News' |
| 6 | import { TechnicalsView } from './Technicals' |
| 7 | |
| 8 | // Financial Modeling Prep over its own connection, fmp/mcp, FMP's MCP server cut to three of its |
| 9 | // tools: one source per tool the connection offers, and a view for two of them, each about one |
| 10 | // company. Form 13F has a source and no view: FMP keeps it behind its Ultimate plan, and a |
| 11 | // view that has only ever been refused is not worth its space. The sources carry no description of |
| 12 | // their own: an MCP source without one borrows the server's, which lists the group's endpoints and |
| 13 | // what each requires, beside the server's own schema, whose `endpoint` names them. The orchestrator |
| 14 | // gets fmp__form-13f, fmp__news, and fmp__technical-indicators, and places the views instead of |
| 15 | // calling them for what a view shows. |
| 16 | |
| 17 | const form13f = defineSource({ mcp: 'mcp', tool: 'form13F' }) |
| 18 | const news = defineSource({ mcp: 'mcp', tool: 'news' }) |
| 19 | const technicalIndicators = defineSource({ mcp: 'mcp', tool: 'technicalIndicators' }) |
| 20 | |
| 21 | const Symbol = z.string().default('') |
| 22 | const Amount = z.number().nullable() |
| 23 | |
| 24 | const NewsState = z.object({ symbol: Symbol, limit: z.number().int().min(1).max(50).optional() }) |
| 25 | const NewsOutput = z.object({ |
| 26 | symbol: z.string(), |
| 27 | count: z.number(), |
| 28 | items: z.array(z.object({ date: z.string(), site: z.string(), title: z.string(), url: z.string() })), |
| 29 | }) |
| 30 | |
| 31 | const TechnicalsState = z.object({ |
| 32 | symbol: Symbol, |
| 33 | timeframe: z.enum(TIMEFRAMES).optional(), |
| 34 | days: z.number().int().min(1).max(365).optional(), |
| 35 | indicators: z.array(z.object({ kind: z.enum(KINDS), period: z.number().int().min(1).max(500) })).max(6).optional(), |
| 36 | }) |
| 37 | const TechnicalsOutput = z.object({ |
| 38 | symbol: z.string(), |
| 39 | timeframe: z.enum(TIMEFRAMES), |
| 40 | date: z.string().nullable(), |
| 41 | close: Amount, |
| 42 | values: z.record(z.string(), Amount), |
| 43 | rows: z.number(), |
| 44 | }) |
| 45 | |
| 46 | export default definePlugin({ |
| 47 | id: 'fmp', |
| 48 | secrets: { apikey: { label: 'FMP API key' } }, |
| 49 | connections: { |
| 50 | // FMP's MCP server, streamable HTTP and stateful, cut to three of its tools. FMP reads the key |
| 51 | // from the URL and nowhere else (a header is refused), so it rides there. FMP accepts any key at |
| 52 | // connect, so a wrong one shows as ready and fails at the first call, saying so. |
| 53 | mcp: defineConnection({ |
| 54 | url: 'https://financialmodelingprep.com/mcp?apikey=${secret:apikey}', |
| 55 | tools: ['form13F', 'news', 'technicalIndicators'], |
| 56 | }), |
| 57 | }, |
| 58 | sources: { 'form-13f': form13f, news, 'technical-indicators': technicalIndicators }, |
| 59 | views: { |
| 60 | news: defineView(NewsView, { |
| 61 | title: 'News', |
| 62 | state: NewsState, |
| 63 | output: NewsOutput, |
| 64 | instructions: NEWS_INSTRUCTIONS, |
| 65 | renders: [news], |
| 66 | summarize: newsSummary, |
| 67 | }), |
| 68 | technicals: defineView(TechnicalsView, { |
| 69 | title: 'Technicals', |
| 70 | state: TechnicalsState, |
| 71 | output: TechnicalsOutput, |
| 72 | instructions: TECHNICALS_INSTRUCTIONS, |
| 73 | renders: [technicalIndicators], |
| 74 | summarize: technicalsSummary, |
| 75 | }), |
| 76 | }, |
| 77 | }) |