stockmatrix-mcp 0.2.0 → 0.4.0

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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # stockmatrix-mcp
2
2
 
3
- MCP server for Korean stock market theme analysis. Track 250+ KOSPI/KOSDAQ investment themes with lifecycle scores, trend data, related stocks, and news — all through natural conversation with AI.
3
+ Ask about Korean stock market themes in natural conversation with AI. Track 250+ KOSPI/KOSDAQ investment themes, get daily movers, compare themes side-by-side, and see predictions — all through Claude, Cursor, or any MCP-compatible AI agent.
4
4
 
5
- Scores are computed using **TLI (Theme Lifecycle Index)** — a Bayesian-optimized algorithm combining search interest, news momentum, market volatility, and stock activity into a 0-100 score with lifecycle stage classification.
5
+ Powered by **TLI (Theme Lifecycle Index)** — a Bayesian-optimized scoring algorithm combining search interest, news momentum, market volatility, and stock activity into a 0-100 score with lifecycle stage classification.
6
6
 
7
7
  ## Quick Start
8
8
 
@@ -57,65 +57,89 @@ After setup, just ask in natural language:
57
57
 
58
58
  | Prompt | What happens |
59
59
  |--------|-------------|
60
- | "요즘 한국 주식시장에서 뜨는 테마 뭐야?" | Top trending themes with scores |
60
+ | "요즘 뜨는 테마 TOP 5" | Top 5 themes ranked by TLI score |
61
+ | "오늘 한국 테마 시장 요약해줘" | AI-optimized market overview |
62
+ | "어제 대비 가장 많이 오른 테마는?" | Daily score movers and stage transitions |
61
63
  | "AI 관련 테마 찾아줘" | Search AI-related themes |
62
- | "반도체 테마 최근 한달 추세 어때?" | 30-day score history |
63
- | "삼성전자가 속한 테마 알려줘" | Themes linked to Samsung (005930) |
64
- | "성장 단계인 테마만 보여줘" | Growth-stage themes only |
65
- | "방산 테마 상세 정보" | Score, stocks, news for defense theme |
66
- | "TLI 점수는 어떻게 계산돼?" | Algorithm methodology |
64
+ | "반도체 vs 2차전지 비교해줘" | Side-by-side theme comparison |
65
+ | "앞으로 오를 테마 알려줘" | Rising predictions with analog evidence |
66
+ | "삼성전자가 속한 테마" | Stock-to-theme lookup (auto-detects 6-digit codes) |
67
+ | "방산 테마 상세 정보" | Score breakdown, stocks, news, comparisons |
68
+ | "반도체 테마 최근 한달 추세" | 30-day score history |
69
+ | "TLI 점수는 어떻게 계산돼?" | Algorithm methodology (tip: use `section=scoring` to save tokens) |
67
70
  | "What are the hottest stock themes in Korea?" | Works in English too |
68
- | "Which themes is SK Hynix (000660) part of?" | Stock-to-theme lookup |
69
71
 
70
72
  ## Available Tools
71
73
 
72
74
  ### `get_theme_ranking`
73
-
74
- Get theme rankings by lifecycle stage.
75
+ Get theme rankings by lifecycle stage with limit and sort options.
75
76
 
76
77
  | Parameter | Type | Required | Description |
77
78
  |-----------|------|----------|-------------|
78
79
  | `stage` | string | No | `emerging` / `growth` / `peak` / `decline` / `reigniting` |
80
+ | `limit` | number | No | Results per stage (1-50, default: 10) |
81
+ | `sort` | string | No | `score` / `change7d` / `newsCount7d` (default: score) |
79
82
 
80
- ### `search_themes`
83
+ ### `get_market_summary`
84
+ Get an AI-optimized market overview with top themes (includes themeId for chaining).
81
85
 
82
- Search themes by keyword (Korean or English).
86
+ ### `get_theme_changes`
87
+ Get daily or weekly score movers, stage transitions, and newly emerging themes.
83
88
 
84
89
  | Parameter | Type | Required | Description |
85
90
  |-----------|------|----------|-------------|
86
- | `query` | string | Yes | e.g. `"AI"`, `"반도체"`, `"2차전지"`, `"삼성전자"` |
91
+ | `period` | string | No | `1d` (default) / `7d` |
87
92
 
88
- ### `get_theme_detail`
93
+ ### `compare_themes`
94
+ Compare 2-5 themes side-by-side with scores, stocks, sparklines, and similarity.
95
+
96
+ | Parameter | Type | Required | Description |
97
+ |-----------|------|----------|-------------|
98
+ | `theme_ids` | string[] | Yes | Array of 2-5 theme UUIDs |
89
99
 
90
- Get detailed info: score breakdown (4 components), stage, prediction, stocks, news, comparisons.
100
+ ### `get_predictions`
101
+ Get themes predicted to rise, peak, or cool based on historical analog matching.
91
102
 
92
103
  | Parameter | Type | Required | Description |
93
104
  |-----------|------|----------|-------------|
94
- | `theme_id` | string (UUID) | Yes | Theme UUID from ranking or search |
105
+ | `phase` | string | No | `rising` / `hot` / `cooling` (default: all) |
95
106
 
96
- ### `get_theme_history`
107
+ ### `search_themes`
108
+ Search themes by keyword, stock name, or stock code.
97
109
 
98
- Get 30-day score history for trend analysis.
110
+ | Parameter | Type | Required | Description |
111
+ |-----------|------|----------|-------------|
112
+ | `query` | string | Yes | e.g. `"AI"`, `"반도체"`, `"삼성전자"`, `"005930"` |
113
+
114
+ ### `search_stocks`
115
+ Search stocks by company name or 6-digit code, with related theme preview. Automatically performs stock-to-theme lookup for 6-digit codes.
99
116
 
100
117
  | Parameter | Type | Required | Description |
101
118
  |-----------|------|----------|-------------|
102
- | `theme_id` | string (UUID) | Yes | Theme UUID |
119
+ | `query` | string | Yes | e.g. `"삼성전자"`, `"SK하이닉스"`, `"005930"` |
120
+
121
+ ### `get_theme_detail`
122
+ Get detailed analysis: score breakdown (4 components), stage, prediction, stocks, news, comparisons.
103
123
 
104
- ### `get_stock_theme`
124
+ | Parameter | Type | Required | Description |
125
+ |-----------|------|----------|-------------|
126
+ | `theme_id` | string (UUID) | Yes | Theme UUID from ranking or search |
105
127
 
106
- Find themes a specific stock belongs to.
128
+ ### `get_theme_history`
129
+ Get 30-day score history for trend analysis.
107
130
 
108
131
  | Parameter | Type | Required | Description |
109
132
  |-----------|------|----------|-------------|
110
- | `symbol` | string | Yes | 6-digit code, e.g. `"005930"` (Samsung), `"000660"` (SK Hynix) |
133
+ | `theme_id` | string (UUID) | Yes | Theme UUID |
111
134
 
112
135
  ### `get_methodology`
113
-
114
- Get TLI algorithm documentation — scoring, stages, stabilization, comparison, prediction.
136
+ Get TLI algorithm documentation — scoring, stages, stabilization, comparison, prediction, data sources, and more.
115
137
 
116
138
  | Parameter | Type | Required | Description |
117
139
  |-----------|------|----------|-------------|
118
- | `section` | string | No | `scoring` / `stabilization` / `stages` / `comparison` / `prediction` / `all` |
140
+ | `section` | string | No | `scoring` / `stages` / `comparison` / `prediction` / `all` (default: all) |
141
+
142
+ > Tip: Use `section=scoring` to get just the scoring algorithm and save context tokens.
119
143
 
120
144
  ## Scoring Algorithm
121
145
 
@@ -128,13 +152,13 @@ TLI scores (0-100) are a weighted sum of 4 components, optimized via Bayesian Op
128
152
  | Volatility | 10.4% | Interest time-series |
129
153
  | Stock Activity | 22.6% | Naver Finance |
130
154
 
131
- Scores are stabilized through **Cautious Decay** (3-signal majority vote prevents false drops), **Bollinger Band Clamp** (limits daily change), and **Age-adaptive EMA** (newer themes react faster).
155
+ Scores are stabilized through **Cautious Decay** (3-signal majority vote), **Bollinger Band Clamp** (limits daily change), and **Age-adaptive EMA** (newer themes react faster).
132
156
 
133
157
  ## Lifecycle Stages
134
158
 
135
159
  ```
136
- Dormant Emerging Growth Peak Decline Dormant
137
-
160
+ Dormant -> Emerging -> Growth -> Peak -> Decline -> Dormant
161
+ |
138
162
  Reigniting
139
163
  ```
140
164
 
@@ -144,6 +168,9 @@ Stage transitions require 2 consecutive days of the same candidate (hysteresis)
144
168
 
145
169
  - **250+ themes** across KOSPI & KOSDAQ
146
170
  - **Daily updates** — scores, news, stock mappings
171
+ - **Stock lookup** by company name or 6-digit code
172
+ - **AI market summary** for first-call overview
173
+ - **Predictions** with historical analog matching
147
174
  - **Sources**: Naver DataLab, Naver Finance, Naver News
148
175
 
149
176
  ## Configuration
@@ -0,0 +1,7 @@
1
+ interface Cache {
2
+ get: <T = unknown>(key: string) => T | undefined;
3
+ set: <T = unknown>(key: string, value: T, ttlMs: number) => void;
4
+ readonly size: number;
5
+ }
6
+ export declare const createCache: (maxSize?: number) => Cache;
7
+ export {};
package/dist/cache.js ADDED
@@ -0,0 +1,36 @@
1
+ export const createCache = (maxSize = 50) => {
2
+ const store = new Map();
3
+ const evictExpired = () => {
4
+ const now = Date.now();
5
+ for (const [key, entry] of store) {
6
+ if (entry.expiresAt <= now) {
7
+ store.delete(key);
8
+ }
9
+ }
10
+ };
11
+ return {
12
+ get(key) {
13
+ const entry = store.get(key);
14
+ if (!entry)
15
+ return undefined;
16
+ if (entry.expiresAt <= Date.now()) {
17
+ store.delete(key);
18
+ return undefined;
19
+ }
20
+ return entry.value;
21
+ },
22
+ set(key, value, ttlMs) {
23
+ if (store.size >= maxSize) {
24
+ evictExpired();
25
+ }
26
+ if (store.size >= maxSize) {
27
+ const oldest = store.keys().next().value;
28
+ store.delete(oldest);
29
+ }
30
+ store.set(key, { value, expiresAt: Date.now() + ttlMs });
31
+ },
32
+ get size() {
33
+ return store.size;
34
+ },
35
+ };
36
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { createRequire } from 'node:module';
5
+ import { registerGetThemeRanking } from './tools/get-theme-ranking.js';
6
+ import { registerGetThemeDetail } from './tools/get-theme-detail.js';
7
+ import { registerGetThemeHistory } from './tools/get-theme-history.js';
8
+ import { registerSearchThemes } from './tools/search-themes.js';
9
+ import { registerSearchStocks } from './tools/search-stocks.js';
10
+ import { registerGetMarketSummary } from './tools/get-market-summary.js';
11
+ import { registerGetMethodology } from './tools/get-methodology.js';
12
+ import { registerGetThemeChanges } from './tools/get-theme-changes.js';
13
+ import { registerCompareThemes } from './tools/compare-themes.js';
14
+ import { registerGetPredictions } from './tools/get-predictions.js';
15
+ const require = createRequire(import.meta.url);
16
+ const { version } = require('../package.json');
17
+ const server = new McpServer({
18
+ name: 'stockmatrix-mcp',
19
+ version,
20
+ });
21
+ registerGetThemeRanking(server);
22
+ registerGetThemeDetail(server);
23
+ registerGetThemeHistory(server);
24
+ registerSearchThemes(server);
25
+ registerSearchStocks(server);
26
+ registerGetMarketSummary(server);
27
+ registerGetMethodology(server);
28
+ registerGetThemeChanges(server);
29
+ registerCompareThemes(server);
30
+ registerGetPredictions(server);
31
+ const main = async () => {
32
+ const transport = new StdioServerTransport();
33
+ await server.connect(transport);
34
+ console.error(`StockMatrix MCP server v${version} running on stdio`);
35
+ };
36
+ main().catch((error) => {
37
+ console.error('Fatal error in main():', error);
38
+ process.exit(1);
39
+ });
@@ -1,4 +1,7 @@
1
- export declare const fetchApi: <T = unknown>(path: string, params?: Record<string, string>) => Promise<T>;
1
+ import type { ZodType } from 'zod';
2
+ export declare const fetchApi: <T = unknown>(path: string, params?: Record<string, string>, schema?: ZodType<T>) => Promise<T>;
2
3
  /** JSON 직렬화 with optional context header for AI agents */
3
4
  export declare const formatResult: (data: unknown, context?: string) => string;
4
5
  export declare const formatError: (error: unknown) => string;
6
+ /** 빈 결과에 가이던스 메시지를 포함하는 포맷터 */
7
+ export declare const formatEmptyResult: (context: string, guidance: string) => string;
@@ -1,6 +1,9 @@
1
1
  import { createRequire } from 'node:module';
2
+ import { createCache } from './cache.js';
2
3
  const require = createRequire(import.meta.url);
3
4
  const { version } = require('../package.json');
5
+ const apiCache = createCache(50);
6
+ const CACHE_TTL_MS = 3_600_000; // 1 hour
4
7
  const BASE_URL = process.env.STOCKMATRIX_API_URL || 'https://stockmatrix.co.kr';
5
8
  const MCP_USER_AGENT = `stockmatrix-mcp/${version}`;
6
9
  // 시작 시 URL 유효성 검증
@@ -26,13 +29,18 @@ const isRetryable = (error) => {
26
29
  return false;
27
30
  };
28
31
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
29
- export const fetchApi = async (path, params) => {
32
+ export const fetchApi = async (path, params, schema) => {
30
33
  const url = new URL(path, BASE_URL);
31
34
  if (params) {
32
35
  for (const [key, value] of Object.entries(params)) {
33
36
  url.searchParams.set(key, value);
34
37
  }
35
38
  }
39
+ const cacheKey = url.toString();
40
+ const cached = apiCache.get(cacheKey);
41
+ if (cached !== undefined) {
42
+ return schema ? schema.parse(cached) : cached;
43
+ }
36
44
  let lastError;
37
45
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
38
46
  if (attempt > 0) {
@@ -67,9 +75,13 @@ export const fetchApi = async (path, params) => {
67
75
  if (!wrapped.success) {
68
76
  throw new Error(wrapped.error?.message || 'API returned unsuccessful response');
69
77
  }
70
- return wrapped.data;
78
+ const result = schema ? schema.parse(wrapped.data) : wrapped.data;
79
+ apiCache.set(cacheKey, result, CACHE_TTL_MS);
80
+ return result;
71
81
  }
72
- return json;
82
+ const rawResult = schema ? schema.parse(json) : json;
83
+ apiCache.set(cacheKey, rawResult, CACHE_TTL_MS);
84
+ return rawResult;
73
85
  }
74
86
  catch (error) {
75
87
  lastError = error;
@@ -92,3 +104,7 @@ export const formatError = (error) => {
92
104
  const message = error instanceof Error ? error.message : String(error);
93
105
  return `Error: ${message}`;
94
106
  };
107
+ /** 빈 결과에 가이던스 메시지를 포함하는 포맷터 */
108
+ export const formatEmptyResult = (context, guidance) => {
109
+ return `${context}\n\n${JSON.stringify([], null, 2)}\n\n${guidance}`;
110
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,2 @@
1
- #!/usr/bin/env node
2
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
2
  export declare const createSandboxServer: () => McpServer;
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
- #!/usr/bin/env node
2
1
  import { createRequire } from 'node:module';
3
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
3
  import { registerGetThemeRanking } from './tools/get-theme-ranking.js';
6
4
  import { registerGetThemeDetail } from './tools/get-theme-detail.js';
7
5
  import { registerGetThemeHistory } from './tools/get-theme-history.js';
8
6
  import { registerSearchThemes } from './tools/search-themes.js';
9
- import { registerGetStockTheme } from './tools/get-stock-theme.js';
7
+ import { registerSearchStocks } from './tools/search-stocks.js';
8
+ import { registerGetMarketSummary } from './tools/get-market-summary.js';
10
9
  import { registerGetMethodology } from './tools/get-methodology.js';
10
+ import { registerGetThemeChanges } from './tools/get-theme-changes.js';
11
+ import { registerCompareThemes } from './tools/compare-themes.js';
12
+ import { registerGetPredictions } from './tools/get-predictions.js';
11
13
  const require = createRequire(import.meta.url);
12
14
  const { version } = require('../package.json');
13
15
  const createServer = () => {
@@ -19,18 +21,12 @@ const createServer = () => {
19
21
  registerGetThemeDetail(s);
20
22
  registerGetThemeHistory(s);
21
23
  registerSearchThemes(s);
22
- registerGetStockTheme(s);
24
+ registerSearchStocks(s);
25
+ registerGetMarketSummary(s);
23
26
  registerGetMethodology(s);
27
+ registerGetThemeChanges(s);
28
+ registerCompareThemes(s);
29
+ registerGetPredictions(s);
24
30
  return s;
25
31
  };
26
32
  export const createSandboxServer = () => createServer();
27
- const server = createServer();
28
- const main = async () => {
29
- const transport = new StdioServerTransport();
30
- await server.connect(transport);
31
- console.error(`StockMatrix MCP server v${version} running on stdio`);
32
- };
33
- main().catch((error) => {
34
- console.error('Fatal error in main():', error);
35
- process.exit(1);
36
- });
@@ -1,2 +1,2 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare const registerGetStockTheme: (server: McpServer) => void;
2
+ export declare const registerCompareThemes: (server: McpServer) => void;
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ import { fetchApi, formatResult, formatError } from '../fetch-helper.js';
3
+ const CONTEXT = `[StockMatrix Theme Comparison]
4
+ Compare 2–5 Korean stock market themes side-by-side.
5
+ Shows each theme's current TLI score, lifecycle stage, 7-day sparkline,
6
+ pairwise similarity (from comparison algorithm), overlapping stocks, and any warnings.
7
+ Use when the user asks to compare or contrast multiple themes.`;
8
+ export const registerCompareThemes = (server) => {
9
+ server.tool('compare_themes', `Compare 2–5 Korean stock market themes side-by-side with lifecycle scores, similarity, and overlapping stocks.
10
+
11
+ Use when the user asks:
12
+ - Compare semiconductor and AI themes
13
+ - How similar are these themes?
14
+ - 반도체 vs AI 테마 비교, 테마 간 유사도
15
+ - Which theme is stronger right now?
16
+ - Do these themes share the same stocks?
17
+
18
+ Returns each theme's score/stage/sparkline, pairwise similarity scores, and overlapping stocks.`, {
19
+ theme_ids: z
20
+ .array(z.string().uuid())
21
+ .min(2)
22
+ .max(5)
23
+ .describe('Array of 2–5 theme UUIDs to compare'),
24
+ }, async ({ theme_ids }) => {
25
+ try {
26
+ const data = await fetchApi('/api/tli/compare', {
27
+ ids: theme_ids.join(','),
28
+ });
29
+ return {
30
+ content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
31
+ };
32
+ }
33
+ catch (error) {
34
+ return {
35
+ content: [{ type: 'text', text: formatError(error) }],
36
+ isError: true,
37
+ };
38
+ }
39
+ });
40
+ };
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare const registerGetMarketSummary: (server: McpServer) => void;
@@ -0,0 +1,30 @@
1
+ import { fetchApi, formatResult, formatError } from '../fetch-helper.js';
2
+ const CONTEXT = `[StockMatrix Market Summary]
3
+ High-level Korean stock market theme briefing for AI agents.
4
+ Use as a first call when the user asks what is hot, what the market looks like today, or wants a concise overview before drilling into a theme.
5
+ Includes stage distribution, top themes, market coverage, endpoint references, and citation/disclaimer metadata.
6
+ Each theme in the response includes themeId for chaining to get_theme_detail.`;
7
+ export const registerGetMarketSummary = (server) => {
8
+ server.tool('get_market_summary', `Get an AI-optimized summary of the Korean stock theme market.
9
+
10
+ Use when the user asks:
11
+ - What's happening in Korean stock themes right now?
12
+ - Give me a market overview before drilling down
13
+ - 오늘 한국 테마 시장 요약
14
+ - 현재 뜨는 테마와 시장 분포를 한 번에 보고 싶어
15
+
16
+ Returns a concise market overview, stage distribution, top themes, endpoint references, citation metadata, and disclaimer text.`, {}, async () => {
17
+ try {
18
+ const data = await fetchApi('/api/ai/summary');
19
+ return {
20
+ content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
21
+ };
22
+ }
23
+ catch (error) {
24
+ return {
25
+ content: [{ type: 'text', text: formatError(error) }],
26
+ isError: true,
27
+ };
28
+ }
29
+ });
30
+ };
@@ -1,80 +1,34 @@
1
1
  import { z } from 'zod';
2
- const METHODOLOGY = {
3
- name: 'TLI (Theme Lifecycle Index)',
4
- version: '2.0 — Bayesian Optimized',
5
- description: 'Quantitative index tracking Korean stock market theme lifecycles using public data sources.',
2
+ import { fetchApi, formatResult } from '../fetch-helper.js';
3
+ const SECTIONS = [
4
+ 'scoring',
5
+ 'stabilization',
6
+ 'stages',
7
+ 'comparison',
8
+ 'prediction',
9
+ 'data_sources',
10
+ 'update_schedule',
11
+ 'runtime',
12
+ 'data_flow',
13
+ 'database_tables',
14
+ 'limitations',
15
+ 'all',
16
+ ];
17
+ const METHODOLOGY_FALLBACK = {
18
+ fallback: true,
6
19
  scoring: {
7
20
  range: '0-100',
8
21
  components: [
9
- { name: 'interest', weight: '30.4%', source: 'Naver DataLab', method: '7-day search volume average vs 30-day baseline, sigmoid-normalized. Batch self-normalization applied (DataLab 5-keyword batch limit).' },
10
- { name: 'newsMomentum', weight: '36.6%', source: 'Naver News', method: 'Log-scale news volume + weekly article count change rate.' },
11
- { name: 'volatility', weight: '10.4%', source: 'Interest time-series', method: 'Standard deviation of interest values, sigmoid-normalized.' },
12
- { name: 'activity', weight: '22.6%', source: 'Naver Finance', method: 'Related stock price change rates, trading volume intensity, and data coverage cross-signal.' },
22
+ { name: 'interest', weight: '30.4%' },
23
+ { name: 'newsMomentum', weight: '36.6%' },
24
+ { name: 'volatility', weight: '10.4%' },
25
+ { name: 'activity', weight: '22.6%' },
13
26
  ],
14
- optimization: 'Weights derived via Bayesian Optimization (Optuna TPE sampler, 2-stage hierarchical search: 80 trials core params + 120 trials fine-tune). Validated with walk-forward train/val split with 7-day gap to prevent data leakage.',
15
- accuracy: 'Growth/Decline Directional Accuracy (GDDA): ~66% on validation set.',
16
27
  },
17
- stabilization: {
18
- pipeline: 'Cautious Decay → Bollinger Band Clamp → EMA Smoothing (applied in this fixed order)',
19
- cautiousDecay: {
20
- description: 'Prevents false score drops from data gaps or temporary noise.',
21
- mechanism: '3 independent binary signals checked on score decline: (1) interest slope < 0, (2) this week news < last week news, (3) directional volatility index < 0.4. Decline confirmed only if 2+ signals agree (majority vote). Otherwise, previous score × 0.947 used as floor.',
22
- },
23
- bollingerClamp: {
24
- description: 'Limits daily score change to prevent spikes.',
25
- mechanism: 'Max daily change = max(min_daily_change, 2 × σ of recent smoothed scores).',
26
- },
27
- emaScheduling: {
28
- description: 'Theme age-adaptive smoothing.',
29
- mechanism: 'EMA alpha linearly interpolated: new themes (0 days) α=0.6 (reactive) → mature themes (30+ days) α=0.3 (stable). Null first_spike_date uses default α=0.417.',
30
- },
31
- },
32
- stages: {
33
- order: 'Dormant → Emerging → Growth → Peak → Decline (→ Dormant or Reigniting)',
34
- thresholds: {
35
- dormant: { score: '< 10', condition: 'AND trend ≠ rising' },
36
- emerging: { score: 'default', condition: 'Does not match other stage criteria' },
37
- growth: { score: '≥ 40', condition: 'AND stable/rising trend' },
38
- peak: { score: '≥ 71', condition: 'OR (≥ 50 AND stable/rising AND news > 30 articles)' },
39
- decline: { condition: 'Falling trend AND score < 86% of level score AND news declining' },
40
- reigniting: { condition: 'Transition from Decline to Emerging/Growth' },
41
- },
42
- hysteresis: '2 consecutive days of same candidate stage required for transition (prevents 1-day noise).',
43
- markov: 'Only allowed transitions: Dormant→Emerging, Emerging→Growth/Dormant, Growth→Peak/Decline, Peak→Decline/Growth, Decline→Dormant/Emerging/Growth. Data gap ≥ 3 days relaxes constraints.',
44
- },
45
- comparison: {
46
- method: '3-Pillar similarity analysis',
47
- pillars: [
48
- { name: 'Feature Similarity', weight: 'dynamic (0.40-1.00)', method: 'Mutual Rank (sqrt of bidirectional rank product) with z-score and cosine fallbacks.' },
49
- { name: 'Curve Similarity', weight: 'dynamic (0.00-0.60)', method: 'Shape RMSE (35%) + Derivative Pearson correlation (30%) + DTW distance (35%). Minimum 14 days data required.' },
50
- { name: 'Keyword Similarity', weight: 'display only', method: 'Jaccard coefficient of theme keywords.' },
51
- ],
52
- threshold: '≥ 0.40 to qualify as meaningful comparison. Auto-tuned via comparison_calibration feedback loop.',
53
- },
54
- prediction: {
55
- horizon: '7-day directional outlook',
56
- phases: 'Rising (Emerging + Growth), Hot (Peak), Cooling (Decline + Dormant)',
57
- reliability: 'Rising signals are most accurate. Cooling signals are reference-level only.',
58
- },
59
- dataSources: [
60
- { name: 'Naver DataLab', provides: 'Search interest trends (relative values, 30-day window)' },
61
- { name: 'Naver News', provides: 'Article counts and headlines' },
62
- { name: 'Naver Finance', provides: 'Theme stock listings, prices, trading volumes' },
63
- ],
64
- updateSchedule: {
65
- themeDiscovery: 'Sunday and Wednesday',
66
- scores: 'Daily (full pipeline after market close)',
67
- news: 'Daily (morning + evening)',
68
- stocks: 'Weekdays (full)',
69
- },
70
- limitations: [
71
- 'Naver DataLab 5-keyword batch limit requires self-normalization across batches — precision is limited.',
72
- 'News momentum is article-count-based; sentiment analysis is not included (removed due to low accuracy).',
73
- 'Data collection intervals mean latest market changes may not be immediately reflected.',
74
- ],
75
- disclaimer: 'This algorithm and its results are for informational purposes only, not investment advice. High scores indicate strong theme momentum, not necessarily investment opportunity — they may signal overheating.',
28
+ disclaimer: 'Full methodology unavailable — showing cached summary. Try again later.',
76
29
  };
77
- const SECTIONS = ['scoring', 'stabilization', 'stages', 'comparison', 'prediction', 'all'];
30
+ const CONTEXT = `[StockMatrix TLI Methodology]
31
+ Comprehensive documentation of the TLI (Theme Lifecycle Index) algorithm — scoring, stages, stabilization, comparison, prediction, data sources, pipeline, and database schema.`;
78
32
  export const registerGetMethodology = (server) => {
79
33
  server.tool('get_methodology', `Get the TLI (Theme Lifecycle Index) algorithm methodology — how scores, stages, and predictions work.
80
34
 
@@ -82,28 +36,28 @@ Use when the user asks:
82
36
  - How are theme scores calculated?
83
37
  - What do the lifecycle stages mean?
84
38
  - How does the prediction work?
39
+ - What data sources, schedules, runtime pipeline, or database tables power TLI?
85
40
  - TLI 알고리즘 설명, 점수 산출 방식, 단계 판정 기준
86
41
  - What data sources are used?
42
+ - TLI 수집 파이프라인, 업데이트 주기, 비교 파이프라인, 데이터 테이블
87
43
 
88
- Returns structured documentation of the scoring algorithm, stage determination, stabilization techniques, comparison analysis, and prediction methodology.`, {
44
+ Returns structured documentation of the scoring algorithm, data collection pipeline, runtime orchestration, database tables, stage determination, stabilization techniques, comparison analysis, and prediction methodology.`, {
89
45
  section: z
90
46
  .enum(SECTIONS)
91
47
  .optional()
92
- .describe('Specific section: scoring, stabilization, stages, comparison, prediction, or all (default: all)'),
48
+ .describe('Specific section: scoring, stabilization, stages, comparison, prediction, data_sources, update_schedule, runtime, data_flow, database_tables, limitations, or all (default: all)'),
93
49
  }, async ({ section }) => {
94
- const selected = section || 'all';
95
- if (selected === 'all') {
50
+ try {
51
+ const params = section ? { section } : undefined;
52
+ const data = await fetchApi('/api/tli/methodology', params);
53
+ return {
54
+ content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
55
+ };
56
+ }
57
+ catch {
96
58
  return {
97
- content: [{ type: 'text', text: JSON.stringify(METHODOLOGY, null, 2) }],
59
+ content: [{ type: 'text', text: formatResult(METHODOLOGY_FALLBACK, CONTEXT) }],
98
60
  };
99
61
  }
100
- const sectionData = METHODOLOGY[selected];
101
- const result = {
102
- section: selected,
103
- ...(typeof sectionData === 'object' ? sectionData : { value: sectionData }),
104
- };
105
- return {
106
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
107
- };
108
62
  });
109
63
  };
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare const registerGetPredictions: (server: McpServer) => void;
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod';
2
+ import { fetchApi, formatResult, formatError } from '../fetch-helper.js';
3
+ const CONTEXT = `[StockMatrix Theme Predictions]
4
+ Lifecycle phase predictions for Korean stock themes based on analog comparison forecasting.
5
+ Shows which themes are rising, hot, or cooling with confidence levels and analog evidence.
6
+ Use to identify emerging opportunities (rising) or themes past peak (cooling).
7
+ Chain with get_theme_detail(themeId) for deeper analysis of any predicted theme.`;
8
+ export const registerGetPredictions = (server) => {
9
+ server.tool('get_predictions', `Get lifecycle phase predictions for Korean stock themes.
10
+
11
+ Use when the user asks:
12
+ - Which themes are rising / about to peak / cooling down?
13
+ - Show me predicted hot themes
14
+ - 상승세 테마 예측, 하락 전환 예상 테마
15
+ - 테마 생명주기 예측 결과를 보고 싶어
16
+
17
+ Returns themes with predicted phase (rising/hot/cooling), confidence, expected peak day, and top analog evidence.`, {
18
+ phase: z
19
+ .enum(['rising', 'hot', 'cooling'])
20
+ .optional()
21
+ .describe('Filter by predicted phase: rising (ascending), hot (at peak), cooling (declining)'),
22
+ }, async ({ phase }) => {
23
+ try {
24
+ const params = {};
25
+ if (phase)
26
+ params.phase = phase;
27
+ const data = await fetchApi('/api/tli/predictions', params);
28
+ if (!data.themes || data.themes.length === 0) {
29
+ const guidance = data.guidance || 'Prediction data not yet available.';
30
+ return {
31
+ content: [{ type: 'text', text: formatResult({ ...data, guidance }, CONTEXT) }],
32
+ };
33
+ }
34
+ return {
35
+ content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
36
+ };
37
+ }
38
+ catch (error) {
39
+ return {
40
+ content: [{ type: 'text', text: formatError(error) }],
41
+ isError: true,
42
+ };
43
+ }
44
+ });
45
+ };
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare const registerGetThemeChanges: (server: McpServer) => void;
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import { fetchApi, formatResult, formatError, formatEmptyResult } from '../fetch-helper.js';
3
+ const CONTEXT = `[StockMatrix Theme Changes]
4
+ Daily or weekly score changes. period=1d means vs previous day, 7d means vs 7 days ago.
5
+ movers: rising (score increased) and falling (score decreased), sorted by magnitude.
6
+ stageTransitions: themes that changed lifecycle stage.
7
+ newlyEmerging: themes that entered Emerging stage.
8
+ Use theme IDs with get_theme_detail for deeper analysis.`;
9
+ const isEmpty = (data) => data.movers.rising.length === 0 &&
10
+ data.movers.falling.length === 0 &&
11
+ data.stageTransitions.length === 0 &&
12
+ data.newlyEmerging.length === 0;
13
+ export const registerGetThemeChanges = (server) => {
14
+ server.tool('get_theme_changes', `Get recent score changes and stage transitions for Korean stock themes.
15
+
16
+ Use when the user asks:
17
+ - What themes changed the most today/this week?
18
+ - Which themes are rising or falling?
19
+ - Any stage transitions recently?
20
+ - 오늘 테마 변동, 급등/급락 테마
21
+ - 이번 주 생명주기 단계 변화한 테마
22
+ - 새로 떠오르는 테마 있어?
23
+
24
+ Returns movers (rising/falling by score change), stage transitions, and newly emerging themes.`, {
25
+ period: z
26
+ .enum(['1d', '7d'])
27
+ .optional()
28
+ .describe('Comparison period: 1d = vs yesterday (default), 7d = vs 7 days ago'),
29
+ }, async ({ period }) => {
30
+ try {
31
+ const data = await fetchApi('/api/tli/changes', {
32
+ period: period || '1d',
33
+ });
34
+ if (isEmpty(data)) {
35
+ return {
36
+ content: [
37
+ {
38
+ type: 'text',
39
+ text: formatEmptyResult(CONTEXT, 'No significant changes detected for this period. Try period=7d for a wider window, or use get_theme_ranking for current standings.'),
40
+ },
41
+ ],
42
+ };
43
+ }
44
+ return {
45
+ content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
46
+ };
47
+ }
48
+ catch (error) {
49
+ return {
50
+ content: [{ type: 'text', text: formatError(error) }],
51
+ isError: true,
52
+ };
53
+ }
54
+ });
55
+ };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { fetchApi, formatResult, formatError } from '../fetch-helper.js';
2
+ import { fetchApi, formatResult, formatError, formatEmptyResult } from '../fetch-helper.js';
3
3
  const CONTEXT = `[StockMatrix Theme Detail]
4
4
  Score components (Bayesian-optimized weights):
5
5
  - interest (30.4%): Naver DataLab search volume — 7-day avg vs 30-day baseline
@@ -22,6 +22,11 @@ Use after get_theme_ranking or search_themes to drill into a specific theme. Ans
22
22
  }, async ({ theme_id }) => {
23
23
  try {
24
24
  const data = await fetchApi(`/api/tli/themes/${theme_id}`);
25
+ if (!data) {
26
+ return {
27
+ content: [{ type: 'text', text: formatEmptyResult(CONTEXT, `Theme not found for ID "${theme_id}". Use search_themes to find valid theme IDs.`) }],
28
+ };
29
+ }
25
30
  return {
26
31
  content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
27
32
  };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { fetchApi, formatResult, formatError } from '../fetch-helper.js';
2
+ import { fetchApi, formatResult, formatError, formatEmptyResult } from '../fetch-helper.js';
3
3
  const CONTEXT = `[StockMatrix Theme History — 30-day]
4
4
  Daily TLI scores with stage transitions. Use to identify:
5
5
  - Trend direction: rising scores = growing interest, falling = declining
@@ -20,6 +20,11 @@ Answers: "이 테마 추세가 어때?", "최근 한달 흐름", "is this theme
20
20
  }, async ({ theme_id }) => {
21
21
  try {
22
22
  const data = await fetchApi(`/api/tli/themes/${theme_id}/history`);
23
+ if (!data || (Array.isArray(data) && data.length === 0)) {
24
+ return {
25
+ content: [{ type: 'text', text: formatEmptyResult(CONTEXT, `No history data found for theme "${theme_id}". The theme may be too new or inactive.`) }],
26
+ };
27
+ }
23
28
  return {
24
29
  content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
25
30
  };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { fetchApi, formatResult, formatError } from '../fetch-helper.js';
2
+ import { fetchApi, formatResult, formatError, formatEmptyResult } from '../fetch-helper.js';
3
3
  const VALID_STAGES = [
4
4
  'emerging',
5
5
  'growth',
@@ -17,7 +17,8 @@ const STAGE_DESCRIPTIONS = {
17
17
  const CONTEXT = `[StockMatrix TLI Ranking]
18
18
  Scores: 0-100 (Bayesian-optimized weighted sum of 4 components: interest 30%, news momentum 37%, volatility 10%, activity 23%).
19
19
  Stages: Emerging → Growth → Peak → Decline → Dormant (with possible Reigniting). Stage transitions require 2 consecutive days of same candidate (hysteresis).
20
- Higher score = stronger theme momentum. Stage indicates lifecycle position.`;
20
+ Higher score = stronger theme momentum. Stage indicates lifecycle position.
21
+ The \`summary\` object includes \`signals\` (market mood indicators), \`hottestTheme\` (single highest scorer with 3+ stocks), and \`surging\` (rapidly rising themes).`;
21
22
  export const registerGetThemeRanking = (server) => {
22
23
  server.tool('get_theme_ranking', `Get Korean stock market theme rankings with lifecycle scores (TLI: Theme Lifecycle Index).
23
24
 
@@ -32,13 +33,35 @@ Returns themes ranked by score (0-100) with lifecycle stage and related stocks.
32
33
  .enum(VALID_STAGES)
33
34
  .optional()
34
35
  .describe('Filter by lifecycle stage: emerging (초기 — early interest), growth (성장 — expanding), peak (정점 — maximum attention), decline (하락 — fading), reigniting (재점화 — comeback)'),
35
- }, async ({ stage }) => {
36
+ limit: z
37
+ .number()
38
+ .int()
39
+ .min(1)
40
+ .max(50)
41
+ .optional()
42
+ .describe('Max themes per stage (1-50, default 10)'),
43
+ sort: z
44
+ .enum(['score', 'change7d', 'newsCount7d'])
45
+ .optional()
46
+ .describe('Sort order within each stage: score (default), change7d, newsCount7d'),
47
+ }, async ({ stage, limit, sort }) => {
36
48
  try {
37
- const data = await fetchApi('/api/tli/scores/ranking');
49
+ const params = {};
50
+ if (limit !== undefined)
51
+ params.limit = String(limit);
52
+ if (sort !== undefined)
53
+ params.sort = sort;
54
+ const fetchParams = Object.keys(params).length > 0 ? params : undefined;
55
+ const data = await fetchApi('/api/tli/scores/ranking', fetchParams);
38
56
  if (stage) {
39
57
  const stageData = data[stage];
40
58
  const summary = data.summary;
41
59
  const stageContext = `${CONTEXT}\nFiltered: ${stage} — ${STAGE_DESCRIPTIONS[stage]}`;
60
+ if (!stageData || (Array.isArray(stageData) && stageData.length === 0)) {
61
+ return {
62
+ content: [{ type: 'text', text: formatEmptyResult(stageContext, `No ${stage} themes currently. Try other stages: ${VALID_STAGES.filter(s => s !== stage).join(', ')}.`) }],
63
+ };
64
+ }
42
65
  return {
43
66
  content: [
44
67
  {
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare const registerSearchStocks: (server: McpServer) => void;
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod';
2
+ import { fetchApi, formatResult, formatError, formatEmptyResult } from '../fetch-helper.js';
3
+ const CONTEXT = `[StockMatrix Stock Search]
4
+ Search Korean stocks by company name or 6-digit code, then inspect which themes they belong to.
5
+ For 6-digit codes, returns detailed stock-to-theme lookup. For text queries, returns matching stocks with themes.
6
+ Results include stock identity plus top related themes with lifecycle score and stage.
7
+ Common examples: 삼성전자, SK하이닉스, NAVER, 카카오, 005930, 000660.`;
8
+ const IS_SIX_DIGIT = /^\d{6}$/;
9
+ export const registerSearchStocks = (server) => {
10
+ server.tool('search_stocks', `Search Korean stocks by company name, symbol, or 6-digit stock code, and preview their related themes.
11
+
12
+ Use when the user asks:
13
+ - Find Samsung Electronics / 삼성전자
14
+ - I only know the company name, not the stock code
15
+ - 종목명으로 코드 찾고 관련 테마도 보고 싶어
16
+ - 삼성전자가 속한 테마 알려줘 / what themes is Samsung in?
17
+ - 005930 테마 알려줘 / which themes is this stock part of?
18
+
19
+ For 6-digit stock codes, automatically performs a detailed stock-to-theme lookup (replaces get_stock_theme).
20
+ For text queries, searches by company name and returns matching stocks with theme previews.`, {
21
+ query: z
22
+ .string()
23
+ .min(1)
24
+ .max(200)
25
+ .describe('Company name or 6-digit stock code, e.g. "삼성전자", "SK하이닉스", "005930"'),
26
+ }, async ({ query }) => {
27
+ try {
28
+ if (IS_SIX_DIGIT.test(query.trim())) {
29
+ const [themeData, searchData] = await Promise.all([
30
+ fetchApi(`/api/tli/stocks/${query.trim()}/theme`).catch(() => null),
31
+ fetchApi('/api/tli/stocks/search', { q: query.trim() }).catch(() => null),
32
+ ]);
33
+ if (!themeData && (!searchData || (Array.isArray(searchData) && searchData.length === 0))) {
34
+ return {
35
+ content: [{ type: 'text', text: formatEmptyResult(CONTEXT, `No stock found for code "${query}". Verify the 6-digit Korean stock code.`) }],
36
+ };
37
+ }
38
+ const combined = { stockThemes: themeData, searchResults: searchData };
39
+ return {
40
+ content: [{ type: 'text', text: formatResult(combined, CONTEXT) }],
41
+ };
42
+ }
43
+ const data = await fetchApi('/api/tli/stocks/search', { q: query });
44
+ if (Array.isArray(data) && data.length === 0) {
45
+ return {
46
+ content: [{ type: 'text', text: formatEmptyResult(CONTEXT, `No stocks found for "${query}". Try a different company name or check the spelling.`) }],
47
+ };
48
+ }
49
+ return {
50
+ content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
51
+ };
52
+ }
53
+ catch (error) {
54
+ return {
55
+ content: [{ type: 'text', text: formatError(error) }],
56
+ isError: true,
57
+ };
58
+ }
59
+ });
60
+ };
@@ -1,25 +1,31 @@
1
1
  import { z } from 'zod';
2
- import { fetchApi, formatResult, formatError } from '../fetch-helper.js';
2
+ import { fetchApi, formatResult, formatError, formatEmptyResult } from '../fetch-helper.js';
3
3
  const CONTEXT = `[StockMatrix Theme Search]
4
4
  Results include TLI score (0-100), lifecycle stage, and theme ID for drill-down.
5
5
  Use theme_id with get_theme_detail for full analysis or get_theme_history for 30-day trend.
6
+ Search also matches related stock names and 6-digit stock codes when available.
6
7
  Stages: Emerging (초기) → Growth (성장) → Peak (정점) → Decline (하락), with Reigniting (재점화) for comeback themes.`;
7
8
  export const registerSearchThemes = (server) => {
8
9
  server.tool('search_themes', `Search Korean stock market themes by keyword (Korean or English).
9
10
 
10
- Use when the user asks about a specific sector, industry, or investment theme. Searches theme names and related stock names.
11
+ Use when the user asks about a specific sector, industry, investment theme, stock name, or stock code. Searches theme names, related stock names, and stock symbols.
11
12
 
12
- Examples: "AI", "반도체" (semiconductor), "2차전지" (EV battery), "방산" (defense), "로봇" (robotics), "원자력" (nuclear), "삼성전자" (Samsung).
13
+ Examples: "AI", "반도체" (semiconductor), "2차전지" (EV battery), "방산" (defense), "로봇" (robotics), "원자력" (nuclear), "삼성전자" (Samsung), "005930".
13
14
 
14
15
  Returns matching themes with TLI scores and lifecycle stages. Use the returned theme_id with get_theme_detail or get_theme_history for deeper analysis.`, {
15
16
  query: z
16
17
  .string()
17
18
  .min(1)
18
19
  .max(200)
19
- .describe('Search query — theme name, sector keyword, or stock name (Korean or English)'),
20
+ .describe('Search query — theme name, sector keyword, stock name, or 6-digit stock code'),
20
21
  }, async ({ query }) => {
21
22
  try {
22
23
  const data = await fetchApi('/api/tli/themes', { q: query });
24
+ if (Array.isArray(data) && data.length === 0) {
25
+ return {
26
+ content: [{ type: 'text', text: formatEmptyResult(CONTEXT, `No themes found for "${query}". Try broader keywords like "AI", "반도체", "2차전지".`) }],
27
+ };
28
+ }
23
29
  return {
24
30
  content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
25
31
  };
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "stockmatrix-mcp",
3
3
  "mcpName": "io.github.MongLong0214/stockmatrix-mcp",
4
- "version": "0.2.0",
4
+ "version": "0.4.0",
5
5
  "description": "StockMatrix MCP Server — Korean stock market theme lifecycle analysis (TLI) with Bayesian-optimized scoring for AI agents",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "bin": {
9
- "stockmatrix-mcp": "dist/index.js"
9
+ "stockmatrix-mcp": "dist/cli.js"
10
10
  },
11
11
  "files": [
12
12
  "dist"
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsc",
16
- "start": "node dist/index.js",
16
+ "start": "node dist/cli.js",
17
17
  "prepublishOnly": "npm run build"
18
18
  },
19
19
  "keywords": [
@@ -26,6 +26,11 @@
26
26
  "kosdaq",
27
27
  "ai-agent"
28
28
  ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/MongLong0214/stock-ai-newsletter",
32
+ "directory": "mcp"
33
+ },
29
34
  "homepage": "https://stockmatrix.co.kr/developers",
30
35
  "license": "MIT",
31
36
  "author": "StockMatrix <aistockmatrix@gmail.com>",
@@ -1,31 +0,0 @@
1
- import { z } from 'zod';
2
- import { fetchApi, formatResult, formatError } from '../fetch-helper.js';
3
- const CONTEXT = `[StockMatrix Stock-to-Theme Lookup]
4
- Shows all investment themes a specific Korean stock belongs to, with each theme's TLI score and lifecycle stage.
5
- A stock can belong to multiple themes simultaneously. Use the returned theme_id with get_theme_detail for full analysis.
6
- Common codes: 005930 (삼성전자), 000660 (SK하이닉스), 373220 (LG에너지솔루션), 035420 (NAVER), 035720 (카카오).`;
7
- export const registerGetStockTheme = (server) => {
8
- server.tool('get_stock_theme', `Find which investment themes a Korean stock belongs to.
9
-
10
- Use when the user asks about a specific stock by its 6-digit code. Returns all associated themes with TLI scores and lifecycle stages.
11
-
12
- Answers: "삼성전자 무슨 테마야?", "what themes is this stock part of?", "이 종목 관련 테마", "005930 테마 알려줘".`, {
13
- symbol: z
14
- .string()
15
- .regex(/^\d{6}$/, 'Korean stock code must be 6 digits')
16
- .describe('6-digit Korean stock code (e.g. "005930" for Samsung, "000660" for SK Hynix)'),
17
- }, async ({ symbol }) => {
18
- try {
19
- const data = await fetchApi(`/api/tli/stocks/${symbol}/theme`);
20
- return {
21
- content: [{ type: 'text', text: formatResult(data, CONTEXT) }],
22
- };
23
- }
24
- catch (error) {
25
- return {
26
- content: [{ type: 'text', text: formatError(error) }],
27
- isError: true,
28
- };
29
- }
30
- });
31
- };