@riskmodels/mcp 1.0.1 → 1.0.4

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.
@@ -10,7 +10,9 @@
10
10
  "/schemas/telemetry-v1.json",
11
11
  "/schemas/estimate-v1.json",
12
12
  "/schemas/decompose-v1.json",
13
+ "/schemas/hedge-levels-v1.json",
13
14
  "/schemas/risk-metadata-v1.json",
14
15
  "/schemas/portfolio-risk-snapshot-v1.json",
16
+ "/schemas/canonical-snapshot-v1.json",
15
17
  "/schemas/portfolio-risk-index-v1.json"
16
18
  ]
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "CanonicalSnapshot",
4
+ "description": "POST /api/snapshot (type=portfolio) — request and top-level response shape (see OPENAPI_SPEC CanonicalSnapshotPortfolioRequest / CanonicalSnapshotResponse).",
5
+ "oneOf": [
6
+ {
7
+ "title": "CanonicalSnapshotPortfolioRequest",
8
+ "type": "object",
9
+ "required": ["type", "portfolio"],
10
+ "properties": {
11
+ "type": { "const": "portfolio" },
12
+ "portfolio": {
13
+ "type": "array",
14
+ "minItems": 1,
15
+ "maxItems": 100,
16
+ "items": {
17
+ "type": "object",
18
+ "required": ["ticker"],
19
+ "properties": {
20
+ "ticker": { "type": "string" },
21
+ "weight": { "type": "number", "exclusiveMinimum": 0 },
22
+ "shares": { "type": "number", "exclusiveMinimum": 0 }
23
+ }
24
+ }
25
+ },
26
+ "lookback_days": { "type": "integer", "minimum": 20, "maximum": 2000, "default": 252 },
27
+ "mode": { "type": "string", "enum": ["frozen"], "default": "frozen" },
28
+ "benchmark": { "type": "string" }
29
+ }
30
+ },
31
+ {
32
+ "title": "CanonicalSnapshotResponseCore",
33
+ "type": "object",
34
+ "required": ["snapshot", "time_behavior", "attribution", "risk_summary", "metadata"],
35
+ "properties": {
36
+ "snapshot": { "type": "object" },
37
+ "time_behavior": { "type": "object" },
38
+ "attribution": { "type": "object" },
39
+ "risk_summary": { "type": "object" },
40
+ "metadata": { "type": "object" },
41
+ "_metadata": { "type": "object" },
42
+ "_agent": { "type": "object" }
43
+ }
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,132 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Decompose Position",
4
+ "description": "Simplified four-layer ERM3 exposure + hedge map for a single ticker. Thin semantic wrapper over the metrics DAL; same billing profile as GET /metrics/{ticker}.",
5
+ "oneOf": [
6
+ { "$ref": "#/definitions/DecomposeRequest" },
7
+ { "$ref": "#/definitions/DecomposeResponse" }
8
+ ],
9
+ "definitions": {
10
+ "LevelHedgeSnapshot": {
11
+ "type": "object",
12
+ "required": [
13
+ "market_hr",
14
+ "sector_hr",
15
+ "subsector_hr",
16
+ "market_er",
17
+ "sector_er",
18
+ "subsector_er",
19
+ "residual_er",
20
+ "hedge_etfs"
21
+ ],
22
+ "properties": {
23
+ "market_hr": { "type": ["number", "null"] },
24
+ "sector_hr": { "type": ["number", "null"] },
25
+ "subsector_hr": { "type": ["number", "null"] },
26
+ "market_er": { "type": ["number", "null"] },
27
+ "sector_er": { "type": ["number", "null"] },
28
+ "subsector_er": { "type": ["number", "null"] },
29
+ "residual_er": { "type": ["number", "null"] },
30
+ "hedge_etfs": {
31
+ "type": "object",
32
+ "required": ["market", "sector", "subsector"],
33
+ "properties": {
34
+ "market": { "type": "string" },
35
+ "sector": { "type": ["string", "null"] },
36
+ "subsector": { "type": ["string", "null"] }
37
+ },
38
+ "additionalProperties": false
39
+ }
40
+ },
41
+ "additionalProperties": false
42
+ },
43
+ "HedgeLevelsBlock": {
44
+ "type": "object",
45
+ "required": ["L1", "L2", "L3"],
46
+ "properties": {
47
+ "L1": { "$ref": "#/definitions/LevelHedgeSnapshot" },
48
+ "L2": { "$ref": "#/definitions/LevelHedgeSnapshot" },
49
+ "L3": { "$ref": "#/definitions/LevelHedgeSnapshot" },
50
+ "recommended_level": { "type": ["string", "null"], "enum": ["L1", "L2", "L3", null] },
51
+ "statistical_lstar": { "type": ["string", "null"], "enum": ["L1", "L2", "L3", null] }
52
+ },
53
+ "additionalProperties": true
54
+ },
55
+ "DecomposeRequest": {
56
+ "type": "object",
57
+ "required": ["ticker"],
58
+ "properties": {
59
+ "ticker": {
60
+ "type": "string",
61
+ "description": "Stock ticker symbol (case-insensitive).",
62
+ "examples": ["NVDA", "AAPL"]
63
+ }
64
+ }
65
+ },
66
+ "DecomposeLayer": {
67
+ "type": "object",
68
+ "required": ["er", "hr", "hedge_etf"],
69
+ "properties": {
70
+ "er": {
71
+ "type": ["number", "null"],
72
+ "description": "Explained-risk variance fraction for this layer (0 to 1)."
73
+ },
74
+ "hr": {
75
+ "type": ["number", "null"],
76
+ "description": "Hedge ratio (dollar ratio). Null for residual."
77
+ },
78
+ "hedge_etf": {
79
+ "type": ["string", "null"],
80
+ "description": "Tradable ETF for this layer. Null for residual."
81
+ }
82
+ }
83
+ },
84
+ "DecomposeResponse": {
85
+ "type": "object",
86
+ "required": [
87
+ "ticker",
88
+ "symbol",
89
+ "data_as_of",
90
+ "teo",
91
+ "exposure",
92
+ "hedge"
93
+ ],
94
+ "properties": {
95
+ "ticker": { "type": "string" },
96
+ "symbol": { "type": "string" },
97
+ "data_as_of": {
98
+ "type": "string",
99
+ "format": "date"
100
+ },
101
+ "teo": {
102
+ "type": "string",
103
+ "format": "date"
104
+ },
105
+ "exposure": {
106
+ "type": "object",
107
+ "required": ["market", "sector", "subsector", "residual"],
108
+ "properties": {
109
+ "market": { "$ref": "#/definitions/DecomposeLayer" },
110
+ "sector": { "$ref": "#/definitions/DecomposeLayer" },
111
+ "subsector": { "$ref": "#/definitions/DecomposeLayer" },
112
+ "residual": { "$ref": "#/definitions/DecomposeLayer" }
113
+ }
114
+ },
115
+ "hedge": {
116
+ "type": "object",
117
+ "additionalProperties": { "type": "number" },
118
+ "description": "Map of hedge-ETF -> dollar ratio (= negative of layer hr). Duplicate ETFs across layers are summed."
119
+ },
120
+ "hedge_levels": { "$ref": "#/definitions/HedgeLevelsBlock" },
121
+ "_metadata": { "type": "object" },
122
+ "_data_health": {
123
+ "type": "object",
124
+ "properties": {
125
+ "er_populated": { "type": "boolean" },
126
+ "er_sum": { "type": ["number", "null"] }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "riskmodels:///schemas/hedge-levels-v1.json",
4
+ "title": "Hedge levels (canonical L1/L2/L3)",
5
+ "description": "Canonical hedge_levels block emitted on GET /metrics/{ticker}, POST /decompose, POST /batch/analyze (success rows), hedge-basket, and related surfaces. Compare standalone L1/L2/L3 hedge solutions without parsing flat l*_mkt_hr wire keys.",
6
+ "oneOf": [{ "$ref": "#/definitions/HedgeLevelsBlock" }],
7
+ "definitions": {
8
+ "LevelHedgeSnapshot": {
9
+ "type": "object",
10
+ "required": [
11
+ "market_hr",
12
+ "sector_hr",
13
+ "subsector_hr",
14
+ "market_er",
15
+ "sector_er",
16
+ "subsector_er",
17
+ "residual_er",
18
+ "hedge_etfs"
19
+ ],
20
+ "properties": {
21
+ "market_hr": {
22
+ "type": ["number", "null"],
23
+ "description": "Dollar-ratio HR — ETF notional per $1 stock."
24
+ },
25
+ "sector_hr": { "type": ["number", "null"] },
26
+ "subsector_hr": { "type": ["number", "null"] },
27
+ "market_er": {
28
+ "type": ["number", "null"],
29
+ "description": "Explained-risk variance fraction for the populated leg(s) when defined."
30
+ },
31
+ "sector_er": { "type": ["number", "null"] },
32
+ "subsector_er": { "type": ["number", "null"] },
33
+ "residual_er": { "type": ["number", "null"] },
34
+ "hedge_etfs": {
35
+ "type": "object",
36
+ "required": ["market", "sector", "subsector"],
37
+ "properties": {
38
+ "market": { "type": "string", "examples": ["SPY"] },
39
+ "sector": { "type": ["string", "null"] },
40
+ "subsector": { "type": ["string", "null"] }
41
+ },
42
+ "additionalProperties": false
43
+ }
44
+ },
45
+ "additionalProperties": false
46
+ },
47
+ "HedgeLevelsBlock": {
48
+ "type": "object",
49
+ "required": ["L1", "L2", "L3"],
50
+ "properties": {
51
+ "L1": { "$ref": "#/definitions/LevelHedgeSnapshot" },
52
+ "L2": { "$ref": "#/definitions/LevelHedgeSnapshot" },
53
+ "L3": { "$ref": "#/definitions/LevelHedgeSnapshot" },
54
+ "recommended_level": {
55
+ "type": ["string", "null"],
56
+ "enum": ["L1", "L2", "L3", null],
57
+ "description": "Economically gated recommendation when emitted by the hedge stack."
58
+ },
59
+ "statistical_lstar": {
60
+ "type": ["string", "null"],
61
+ "enum": ["L1", "L2", "L3", null],
62
+ "description": "Cascade statistical layer pick (orthogonal Lstar)."
63
+ }
64
+ },
65
+ "additionalProperties": true
66
+ }
67
+ }
68
+ }
@@ -3,7 +3,7 @@ type McpContent = {
3
3
  type: "text";
4
4
  text: string;
5
5
  };
6
- type McpToolResult = {
6
+ export type McpToolResult = {
7
7
  content: McpContent[];
8
8
  };
9
9
  type McpPromptResult = {
@@ -13,7 +13,7 @@ type McpPromptResult = {
13
13
  content: McpContent;
14
14
  }>;
15
15
  };
16
- type McpLikeServer = {
16
+ export type McpLikeServer = {
17
17
  registerTool: (name: string, config: Record<string, unknown>, handler: (args: any) => Promise<McpToolResult>) => void;
18
18
  registerResource: (name: string, uri: string, config: Record<string, unknown>, handler: (uri: URL) => Promise<{
19
19
  contents: Array<{
@@ -24,11 +24,13 @@ type McpLikeServer = {
24
24
  }>) => void;
25
25
  registerPrompt?: (name: string, config: Record<string, unknown>, handler: (args: any) => McpPromptResult | Promise<McpPromptResult>) => void;
26
26
  };
27
+ export declare function textResult(payload: unknown): McpToolResult;
28
+ export declare function errorResult(error: unknown): McpToolResult;
27
29
  export declare function createRiskModelsSdk(opts: {
28
30
  apiKey?: string | null;
29
31
  apiBase?: string;
30
32
  }): RiskModelsClient;
31
- export declare function registerRiskModelsTools(sdk: Pick<RiskModelsClient, "decompose" | "compare" | "hedgePosition" | "portfolioDecompose" | "whitepaperExample">, server: McpLikeServer): void;
33
+ export declare function registerRiskModelsTools(sdk: Pick<RiskModelsClient, "decompose" | "getHedgeLevels" | "compare" | "hedgePosition" | "analyzePortfolio" | "hedgePortfolio" | "portfolioDecompose" | "whitepaperExample">, server: McpLikeServer): void;
32
34
  export declare function registerRiskModelsWhitepaperResources(server: McpLikeServer, dataDir: string): void;
33
35
  export declare function registerRiskModelsPrompts(server: McpLikeServer): void;
34
36
  export {};
@@ -35,7 +35,7 @@ const WHITEPAPER_RESOURCES = [
35
35
  },
36
36
  ];
37
37
  const CHART_INSTRUCTION = "If chart_data is present, render the suggested_chart. Use grouped bars for comparisons and bars for single-stock decomposition. Always explain the result in plain English.";
38
- function textResult(payload) {
38
+ export function textResult(payload) {
39
39
  return {
40
40
  content: [
41
41
  {
@@ -50,7 +50,7 @@ function textResult(payload) {
50
50
  ],
51
51
  };
52
52
  }
53
- function errorResult(error) {
53
+ export function errorResult(error) {
54
54
  return textResult({
55
55
  error: error instanceof Error ? error.message : String(error),
56
56
  });
@@ -73,7 +73,8 @@ export function createRiskModelsSdk(opts) {
73
73
  export function registerRiskModelsTools(sdk, server) {
74
74
  server.registerTool("riskmodels_decompose", {
75
75
  title: "RiskModels Single-Stock Decomposition",
76
- description: "Decompose one stock into market, sector, subsector, and residual risk. Returns chart_data, suggested_chart, plain_english, and reproducible api_call metadata.",
76
+ annotations: { readOnlyHint: true },
77
+ description: "L3 four-bet view: decompose one stock into additive market, sector, subsector, and residual layers (same semantics as POST /decompose exposure/hedge). Returns chart_data and plain_english. To compare standalone L1 vs L2 vs L3 hedge solutions (HR/ER + ETF legs), call riskmodels_get_hedge_levels or read hedge_levels on the API response.",
77
78
  inputSchema: {
78
79
  ticker: z.string().min(1).describe("Ticker symbol, e.g. NVDA or AAPL"),
79
80
  },
@@ -85,8 +86,24 @@ export function registerRiskModelsTools(sdk, server) {
85
86
  return errorResult(error);
86
87
  }
87
88
  });
89
+ server.registerTool("riskmodels_get_hedge_levels", {
90
+ title: "RiskModels L1/L2/L3 hedge_levels",
91
+ annotations: { readOnlyHint: true },
92
+ description: "Canonical L1, L2, and L3 hedge snapshots (semantic HR/ER + hedge_etfs) from GET /metrics/{ticker}. Use this when you need to compare which cascade depth to trade, distinct from decompose four-bet exposure.",
93
+ inputSchema: {
94
+ ticker: z.string().min(1).describe("Ticker symbol, e.g. NVDA or AAPL"),
95
+ },
96
+ }, async ({ ticker }) => {
97
+ try {
98
+ return textResult(await sdk.getHedgeLevels(ticker));
99
+ }
100
+ catch (error) {
101
+ return errorResult(error);
102
+ }
103
+ });
88
104
  server.registerTool("riskmodels_compare", {
89
105
  title: "RiskModels Multi-Ticker Comparison",
106
+ annotations: { readOnlyHint: true },
90
107
  description: "Compare tickers across market, sector, subsector, and residual risk layers. Prefer grouped bar charts when chart_data is present.",
91
108
  inputSchema: {
92
109
  tickers: z.array(z.string().min(1)).min(2).max(100).describe("Ticker symbols to compare"),
@@ -101,6 +118,7 @@ export function registerRiskModelsTools(sdk, server) {
101
118
  });
102
119
  server.registerTool("riskmodels_hedge_position", {
103
120
  title: "RiskModels Position Hedge",
121
+ annotations: { readOnlyHint: true },
104
122
  description: "Scale ETF hedge ratios for a ticker to a dollar position. Returns chart-ready hedge notionals.",
105
123
  inputSchema: {
106
124
  ticker: z.string().min(1).describe("Ticker symbol, e.g. NVDA"),
@@ -114,8 +132,62 @@ export function registerRiskModelsTools(sdk, server) {
114
132
  return errorResult(error);
115
133
  }
116
134
  });
135
+ server.registerTool("riskmodels_analyze_portfolio", {
136
+ title: "RiskModels Portfolio hedge_levels aggregate",
137
+ annotations: { readOnlyHint: true },
138
+ description: "Holdings-weighted L1/L2/L3 hedge_levels across names via POST /batch/analyze (hedge_ratios). Returns normalized portfolio.portfolio_hedge_levels and per-ticker blocks when present.",
139
+ inputSchema: {
140
+ positions: z
141
+ .array(z.object({
142
+ ticker: z.string().min(1),
143
+ weight: z.number().positive().optional(),
144
+ dollars: z.number().positive().optional(),
145
+ }))
146
+ .min(1)
147
+ .max(100)
148
+ .describe("Positions with weight or dollars (combined per ticker)"),
149
+ years: z.number().int().min(1).max(30).optional().describe("Batch lookback window, default 1"),
150
+ },
151
+ }, async ({ positions, years }) => {
152
+ try {
153
+ return textResult(await sdk.analyzePortfolio(positions, { years }));
154
+ }
155
+ catch (error) {
156
+ return errorResult(error);
157
+ }
158
+ });
159
+ server.registerTool("riskmodels_hedge_portfolio", {
160
+ title: "RiskModels Portfolio ETF hedge notionals",
161
+ annotations: { readOnlyHint: true },
162
+ description: "Batch hedge_ratios at a chosen cascade level (L1/L2/L3), scale HRs by dollar notionals per ticker, and aggregate ETF USD hedge legs.",
163
+ inputSchema: {
164
+ positions: z
165
+ .array(z.object({
166
+ ticker: z.string().min(1),
167
+ dollars: z.number().positive(),
168
+ }))
169
+ .min(1)
170
+ .max(100),
171
+ level: z.enum(["L1", "L2", "L3"]).optional().describe("Cascade depth; default L3"),
172
+ years: z.number().int().min(1).max(30).optional(),
173
+ },
174
+ }, async ({ positions, level, years }) => {
175
+ try {
176
+ return textResult(await sdk.hedgePortfolio(positions.map((row) => ({
177
+ ticker: row.ticker,
178
+ dollars: row.dollars,
179
+ })), {
180
+ level,
181
+ years,
182
+ }));
183
+ }
184
+ catch (error) {
185
+ return errorResult(error);
186
+ }
187
+ });
117
188
  server.registerTool("riskmodels_portfolio_decompose", {
118
189
  title: "RiskModels Portfolio Decomposition",
190
+ annotations: { readOnlyHint: true },
119
191
  description: "Decompose a weighted portfolio into market, sector, subsector, and residual risk layers.",
120
192
  inputSchema: {
121
193
  positions: z
@@ -138,6 +210,7 @@ export function registerRiskModelsTools(sdk, server) {
138
210
  });
139
211
  server.registerTool("riskmodels_whitepaper_example", {
140
212
  title: "RiskModels Live White-Paper Example",
213
+ annotations: { readOnlyHint: true },
141
214
  description: "Run a live example from the RiskModels white paper. Returns chapter text plus SDK/API output with chart_data.",
142
215
  inputSchema: {
143
216
  exampleId: z
@@ -207,5 +280,5 @@ ${CHART_INSTRUCTION}`));
207
280
  server.registerPrompt("explain_my_portfolio", {
208
281
  title: "Explain My Portfolio",
209
282
  description: "Decompose a portfolio into RiskModels layers.",
210
- }, () => promptText(`Ask me for tickers and weights or dollar notionals, then call riskmodels_portfolio_decompose. Render chart_data using suggested_chart and explain the market, sector, subsector, and residual risk layers.`));
283
+ }, () => promptText(`Ask me for tickers and weights or dollar notionals, then call riskmodels_portfolio_decompose (L3 four-bet aggregation) or riskmodels_analyze_portfolio for holdings-weighted hedge_levels across L1/L2/L3. Render chart_data using suggested_chart and explain the layers.`));
211
284
  }
@@ -430,5 +430,60 @@ export function createMcpServer(opts = {}) {
430
430
  }
431
431
  return { content: [{ type: "text", text: wrapWithMeter(data, meter) }] };
432
432
  });
433
+ server.registerTool("post_snapshot", {
434
+ title: "Canonical Portfolio Snapshot",
435
+ description: "Run a canonical risk snapshot on a portfolio (1–100 positions): L3 variance decomposition (market / sector / subsector / residual / systematic), L3 hedge ratios per position, frozen-weight daily return attribution (gross + market / sector / subsector strips + residual), cumulative return and drawdown over the lookback window, and a risk_summary with dominant drivers, concentration flags, and top exposures. This is the canonical RiskModels public surface — same response shape across UI, CLI, SDK, and agents. Provide either weight or shares for every position (do not mix). Bills as portfolio-risk-snapshot ($0.25 per request).",
436
+ inputSchema: z.object({
437
+ portfolio: z
438
+ .array(z
439
+ .object({
440
+ ticker: z.string().describe("US equity ticker, e.g. NVDA, AAPL, SPY"),
441
+ weight: z
442
+ .number()
443
+ .positive()
444
+ .optional()
445
+ .describe("Positive weight or dollar amount; server normalizes to sum to 1"),
446
+ shares: z
447
+ .number()
448
+ .positive()
449
+ .optional()
450
+ .describe("Share count; converted to weights via latest price_close"),
451
+ })
452
+ .refine((p) => (p.weight != null) !== (p.shares != null), {
453
+ message: "Each position must have exactly one of weight or shares",
454
+ }))
455
+ .min(1)
456
+ .max(100)
457
+ .describe("Portfolio positions. Use weights for every position OR shares for every position — do not mix."),
458
+ lookback_days: z
459
+ .number()
460
+ .int()
461
+ .min(20)
462
+ .max(2000)
463
+ .optional()
464
+ .default(252)
465
+ .describe("Trading days of history for return curves and attribution series (default 252)"),
466
+ benchmark: z
467
+ .string()
468
+ .optional()
469
+ .describe("Optional benchmark ticker (reserved for comparison views)"),
470
+ }),
471
+ }, async ({ portfolio, lookback_days, benchmark }) => {
472
+ const body = {
473
+ type: "portfolio",
474
+ portfolio,
475
+ lookback_days,
476
+ };
477
+ if (benchmark)
478
+ body.benchmark = benchmark;
479
+ const { status, data, meter, error } = await apiCall(opts, "POST", "/snapshot", { body });
480
+ if (error) {
481
+ return { content: [{ type: "text", text: JSON.stringify({ error }) }] };
482
+ }
483
+ if (status >= 400) {
484
+ return { content: [{ type: "text", text: JSON.stringify({ error: `API ${status}`, detail: data }) }] };
485
+ }
486
+ return { content: [{ type: "text", text: wrapWithMeter(data, meter) }] };
487
+ });
433
488
  return server;
434
489
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@riskmodels/mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
+ "mcpName": "io.github.BlueWaterCorp/riskmodels",
4
5
  "description": "MCP server for RiskModels: decompose US equities into four-bet variance shares with ETF hedge ratios",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",
7
8
  "bin": {
8
- "riskmodels-mcp": "./dist/index.js"
9
+ "riskmodels-mcp": "dist/index.js"
9
10
  },
10
11
  "exports": {
11
12
  ".": {
@@ -29,7 +30,7 @@
29
30
  },
30
31
  "dependencies": {
31
32
  "@modelcontextprotocol/sdk": "^1.0.0",
32
- "@riskmodels/sdk": "^0.1.0",
33
+ "@riskmodels/sdk": "^0.1.2",
33
34
  "zod": "^3.23.8"
34
35
  },
35
36
  "devDependencies": {
package/server.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.BlueWaterCorp/riskmodels",
4
+ "title": "RiskModels",
5
+ "description": "US equity risk: decompose any stock into market/sector/subsector/residual bets + ETF hedge ratios.",
6
+ "websiteUrl": "https://riskmodels.app",
7
+ "repository": {
8
+ "url": "https://github.com/BlueWaterCorp/RiskModels_API",
9
+ "source": "github"
10
+ },
11
+ "version": "1.0.4",
12
+ "remotes": [
13
+ {
14
+ "type": "streamable-http",
15
+ "url": "https://riskmodels.app/api/mcp/sse"
16
+ }
17
+ ]
18
+ }
package/src/server.ts CHANGED
@@ -551,5 +551,69 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
551
551
  },
552
552
  );
553
553
 
554
+ server.registerTool(
555
+ "post_snapshot",
556
+ {
557
+ title: "Canonical Portfolio Snapshot",
558
+ description:
559
+ "Run a canonical risk snapshot on a portfolio (1–100 positions): L3 variance decomposition (market / sector / subsector / residual / systematic), L3 hedge ratios per position, frozen-weight daily return attribution (gross + market / sector / subsector strips + residual), cumulative return and drawdown over the lookback window, and a risk_summary with dominant drivers, concentration flags, and top exposures. This is the canonical RiskModels public surface — same response shape across UI, CLI, SDK, and agents. Provide either weight or shares for every position (do not mix). Bills as portfolio-risk-snapshot ($0.25 per request).",
560
+ inputSchema: z.object({
561
+ portfolio: z
562
+ .array(
563
+ z
564
+ .object({
565
+ ticker: z.string().describe("US equity ticker, e.g. NVDA, AAPL, SPY"),
566
+ weight: z
567
+ .number()
568
+ .positive()
569
+ .optional()
570
+ .describe("Positive weight or dollar amount; server normalizes to sum to 1"),
571
+ shares: z
572
+ .number()
573
+ .positive()
574
+ .optional()
575
+ .describe("Share count; converted to weights via latest price_close"),
576
+ })
577
+ .refine((p) => (p.weight != null) !== (p.shares != null), {
578
+ message: "Each position must have exactly one of weight or shares",
579
+ }),
580
+ )
581
+ .min(1)
582
+ .max(100)
583
+ .describe(
584
+ "Portfolio positions. Use weights for every position OR shares for every position — do not mix.",
585
+ ),
586
+ lookback_days: z
587
+ .number()
588
+ .int()
589
+ .min(20)
590
+ .max(2000)
591
+ .optional()
592
+ .default(252)
593
+ .describe("Trading days of history for return curves and attribution series (default 252)"),
594
+ benchmark: z
595
+ .string()
596
+ .optional()
597
+ .describe("Optional benchmark ticker (reserved for comparison views)"),
598
+ }),
599
+ },
600
+ async ({ portfolio, lookback_days, benchmark }) => {
601
+ const body: Record<string, unknown> = {
602
+ type: "portfolio",
603
+ portfolio,
604
+ lookback_days,
605
+ };
606
+ if (benchmark) body.benchmark = benchmark;
607
+ const { status, data, meter, error } = await apiCall(opts, "POST", "/snapshot", { body });
608
+ if (error) {
609
+ return { content: [{ type: "text", text: JSON.stringify({ error }) }] };
610
+ }
611
+ if (status >= 400) {
612
+ return { content: [{ type: "text", text: JSON.stringify({ error: `API ${status}`, detail: data }) }] };
613
+ }
614
+ return { content: [{ type: "text", text: wrapWithMeter(data, meter) }] };
615
+ },
616
+ );
617
+
554
618
  return server;
555
619
  }