@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.
- package/INSTALL.md +3 -2
- package/NPM_REPUBLISH_CHECKLIST.md +221 -0
- package/PUBLISHING.md +83 -0
- package/README.md +20 -3
- package/data/benchmark_master.json +68 -0
- package/data/capabilities.json +729 -4
- package/data/etf_master.json +304 -0
- package/data/openapi.json +11060 -3722
- package/data/schema-paths.json +2 -0
- package/data/schemas/canonical-snapshot-v1.json +46 -0
- package/data/schemas/decompose-v1.json +132 -0
- package/data/schemas/hedge-levels-v1.json +68 -0
- package/dist/lib/mcp/tools/riskmodels-tools.d.ts +5 -3
- package/dist/lib/mcp/tools/riskmodels-tools.js +77 -4
- package/dist/mcp/src/server.js +55 -0
- package/package.json +4 -3
- package/server.json +18 -0
- package/src/server.ts +64 -0
package/data/schema-paths.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|
package/dist/mcp/src/server.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
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.
|
|
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
|
}
|