bitbank-lab-mcp 0.1.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/LICENSE +21 -0
- package/README.md +388 -0
- package/assets/lightweight-charts.standalone.js +7 -0
- package/bin/bitbank-lab-mcp.js +20 -0
- package/lib/cache.ts +70 -0
- package/lib/candle-utils.ts +48 -0
- package/lib/candle-validate.ts +434 -0
- package/lib/conversions.ts +25 -0
- package/lib/datetime.ts +157 -0
- package/lib/depth-analysis.ts +51 -0
- package/lib/error.ts +15 -0
- package/lib/formatter.ts +296 -0
- package/lib/get-depth.ts +111 -0
- package/lib/http.ts +132 -0
- package/lib/indicator-config.ts +39 -0
- package/lib/indicator_buffer.ts +41 -0
- package/lib/indicators.ts +579 -0
- package/lib/logger.ts +120 -0
- package/lib/ma-snapshot-utils.ts +277 -0
- package/lib/math.ts +89 -0
- package/lib/pattern-diagrams.ts +562 -0
- package/lib/result.ts +104 -0
- package/lib/validate.ts +154 -0
- package/lib/volatility.ts +132 -0
- package/package.json +79 -0
- package/src/env.ts +4 -0
- package/src/handlers/analyzeCandlePatternsHandler.ts +383 -0
- package/src/handlers/analyzeFibonacciHandler.ts +54 -0
- package/src/handlers/analyzeIndicatorsHandler.ts +682 -0
- package/src/handlers/analyzeMarketSignalHandler.ts +272 -0
- package/src/handlers/analyzeMyPortfolioHandler.ts +800 -0
- package/src/handlers/detectPatternsHandler.ts +77 -0
- package/src/handlers/detectPatternsViewsHandler.ts +518 -0
- package/src/handlers/getTickersJpyHandler.ts +145 -0
- package/src/handlers/getVolatilityMetricsHandler.ts +234 -0
- package/src/handlers/portfolio/calc.ts +549 -0
- package/src/handlers/portfolio/fetch.ts +318 -0
- package/src/handlers/portfolio/types.ts +170 -0
- package/src/handlers/renderChartSvgHandler.ts +69 -0
- package/src/handlers/runBacktestHandler.ts +70 -0
- package/src/http.ts +107 -0
- package/src/private/auth.ts +104 -0
- package/src/private/client.ts +298 -0
- package/src/private/config.ts +25 -0
- package/src/private/confirmation.ts +185 -0
- package/src/private/schemas.ts +866 -0
- package/src/prompts.ts +2296 -0
- package/src/resources/app-resources.ts +79 -0
- package/src/schema/analysis.ts +942 -0
- package/src/schema/backtest.ts +100 -0
- package/src/schema/base.ts +88 -0
- package/src/schema/candle-validate.ts +135 -0
- package/src/schema/chart.ts +399 -0
- package/src/schema/index.ts +11 -0
- package/src/schema/indicators.ts +125 -0
- package/src/schema/market-data.ts +298 -0
- package/src/schema/patterns.ts +382 -0
- package/src/schema/types.ts +97 -0
- package/src/schemas.d.ts +37 -0
- package/src/schemas.ts +7 -0
- package/src/server.ts +405 -0
- package/src/tool-definition.ts +44 -0
- package/src/tool-registry.ts +174 -0
- package/src/types/express-shim.d.ts +9 -0
- package/src/types/schemas.generated.d.ts +23 -0
- package/tools/analyze_bb_snapshot.ts +385 -0
- package/tools/analyze_candle_patterns.ts +810 -0
- package/tools/analyze_currency_strength.ts +273 -0
- package/tools/analyze_ema_snapshot.ts +183 -0
- package/tools/analyze_fibonacci.ts +530 -0
- package/tools/analyze_ichimoku_snapshot.ts +606 -0
- package/tools/analyze_indicators.ts +691 -0
- package/tools/analyze_market_signal.ts +665 -0
- package/tools/analyze_mtf_fibonacci.ts +273 -0
- package/tools/analyze_mtf_sma.ts +175 -0
- package/tools/analyze_sma_snapshot.ts +146 -0
- package/tools/analyze_stoch_snapshot.ts +276 -0
- package/tools/analyze_support_resistance.ts +817 -0
- package/tools/analyze_volume_profile.ts +546 -0
- package/tools/chart/ichimoku-cloud.ts +113 -0
- package/tools/chart/render-depth.ts +139 -0
- package/tools/chart/render-sub-panels.ts +208 -0
- package/tools/chart/svg-utils.ts +102 -0
- package/tools/detect_macd_cross.ts +691 -0
- package/tools/detect_patterns.ts +424 -0
- package/tools/detect_whale_events.ts +181 -0
- package/tools/get_candles.ts +487 -0
- package/tools/get_flow_metrics.ts +596 -0
- package/tools/get_orderbook.ts +540 -0
- package/tools/get_ticker.ts +132 -0
- package/tools/get_tickers_jpy.ts +240 -0
- package/tools/get_transactions.ts +209 -0
- package/tools/get_volatility_metrics.ts +302 -0
- package/tools/patterns/aftermath.ts +212 -0
- package/tools/patterns/config.ts +151 -0
- package/tools/patterns/detect_doubles.ts +650 -0
- package/tools/patterns/detect_hs.ts +635 -0
- package/tools/patterns/detect_pennants.ts +373 -0
- package/tools/patterns/detect_triangles.ts +820 -0
- package/tools/patterns/detect_triples.ts +633 -0
- package/tools/patterns/detect_wedges.ts +1072 -0
- package/tools/patterns/helpers.ts +517 -0
- package/tools/patterns/index.ts +40 -0
- package/tools/patterns/regression.ts +153 -0
- package/tools/patterns/smoothing.ts +168 -0
- package/tools/patterns/swing.ts +91 -0
- package/tools/patterns/types.ts +193 -0
- package/tools/prepare_chart_data.ts +294 -0
- package/tools/prepare_depth_data.ts +189 -0
- package/tools/private/analyze_my_portfolio.ts +21 -0
- package/tools/private/cancel_order.ts +127 -0
- package/tools/private/cancel_orders.ts +121 -0
- package/tools/private/create_order.ts +236 -0
- package/tools/private/get_margin_positions.ts +134 -0
- package/tools/private/get_margin_status.ts +155 -0
- package/tools/private/get_margin_trade_history.ts +156 -0
- package/tools/private/get_my_assets.ts +207 -0
- package/tools/private/get_my_deposit_withdrawal.ts +500 -0
- package/tools/private/get_my_orders.ts +157 -0
- package/tools/private/get_my_trade_history.ts +229 -0
- package/tools/private/get_order.ts +95 -0
- package/tools/private/get_orders_info.ts +90 -0
- package/tools/private/preview_cancel_order.ts +172 -0
- package/tools/private/preview_cancel_orders.ts +137 -0
- package/tools/private/preview_order.ts +292 -0
- package/tools/render_candle_pattern_diagram.ts +389 -0
- package/tools/render_chart_svg.ts +799 -0
- package/tools/render_depth_svg.ts +274 -0
- package/tools/trading_process/index.ts +7 -0
- package/tools/trading_process/lib/backtest_engine.ts +252 -0
- package/tools/trading_process/lib/equity.ts +131 -0
- package/tools/trading_process/lib/fetch_candles.ts +181 -0
- package/tools/trading_process/lib/sma.ts +62 -0
- package/tools/trading_process/lib/strategies/bb_breakout.ts +141 -0
- package/tools/trading_process/lib/strategies/index.ts +52 -0
- package/tools/trading_process/lib/strategies/macd_cross.ts +256 -0
- package/tools/trading_process/lib/strategies/rsi.ts +133 -0
- package/tools/trading_process/lib/strategies/sma_cross.ts +214 -0
- package/tools/trading_process/lib/strategies/types.ts +118 -0
- package/tools/trading_process/lib/svg_to_png.ts +64 -0
- package/tools/trading_process/render_backtest_chart_generic.ts +729 -0
- package/tools/trading_process/run_backtest.ts +243 -0
- package/tools/trading_process/types.ts +85 -0
- package/tools/validate_candle_data.ts +260 -0
- package/tsconfig.json +17 -0
- package/ui/cancel-confirm/dist/cancel-confirm.html +99 -0
- package/ui/order-confirm/dist/order-confirm.html +99 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { CandleSchema, CandleTypeEnum, NumericSeriesSchema, TrendLabelEnum } from './base.js';
|
|
3
|
+
import type { ValidateCandleDataDataSchema, ValidateCandleDataMetaSchema } from './candle-validate.js';
|
|
4
|
+
import type {
|
|
5
|
+
BollingerBandsSeriesSchema,
|
|
6
|
+
ChartIndicatorsSchema,
|
|
7
|
+
ChartMetaSchema,
|
|
8
|
+
ChartPayloadSchema,
|
|
9
|
+
EmaSeriesFixedSchema,
|
|
10
|
+
IchimokuSeriesSchema,
|
|
11
|
+
RenderChartSvgInputSchema,
|
|
12
|
+
SmaSeriesFixedSchema,
|
|
13
|
+
} from './chart.js';
|
|
14
|
+
import type { GetIndicatorsDataSchema, GetIndicatorsMetaSchema, IndicatorsInternalSchema } from './indicators.js';
|
|
15
|
+
import type {
|
|
16
|
+
GetCandlesDataSchemaOut,
|
|
17
|
+
GetCandlesMetaSchemaOut,
|
|
18
|
+
GetOrderbookDataSchemaOut,
|
|
19
|
+
GetOrderbookMetaSchemaOut,
|
|
20
|
+
GetTickerDataSchemaOut,
|
|
21
|
+
GetTickerMetaSchemaOut,
|
|
22
|
+
KeyPointSchema,
|
|
23
|
+
KeyPointsSchema,
|
|
24
|
+
OrderbookLevelSchema,
|
|
25
|
+
OrderbookLevelWithCumSchema,
|
|
26
|
+
OrderbookNormalizedSchema,
|
|
27
|
+
TickerNormalizedSchema,
|
|
28
|
+
VolumeStatsSchema,
|
|
29
|
+
} from './market-data.js';
|
|
30
|
+
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
32
|
+
// Inferred Types — single source of truth (replaces domain.d.ts)
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
34
|
+
|
|
35
|
+
/** e.g., "btc_jpy" */
|
|
36
|
+
export type Pair = `${string}_${string}`;
|
|
37
|
+
|
|
38
|
+
// --- Core data types ---
|
|
39
|
+
export type CandleType = z.infer<typeof CandleTypeEnum>;
|
|
40
|
+
export type Candle = z.infer<typeof CandleSchema>;
|
|
41
|
+
export type NumericSeries = z.output<typeof NumericSeriesSchema>;
|
|
42
|
+
export type TickerNormalized = z.infer<typeof TickerNormalizedSchema>;
|
|
43
|
+
export type OrderbookLevel = z.infer<typeof OrderbookLevelSchema>;
|
|
44
|
+
export type OrderbookLevelWithCum = z.infer<typeof OrderbookLevelWithCumSchema>;
|
|
45
|
+
export type OrderbookNormalized = z.infer<typeof OrderbookNormalizedSchema>;
|
|
46
|
+
export type TrendLabel = z.infer<typeof TrendLabelEnum>;
|
|
47
|
+
|
|
48
|
+
// --- Indicator series ---
|
|
49
|
+
export type IchimokuSeries = z.infer<typeof IchimokuSeriesSchema>;
|
|
50
|
+
export type BollingerBandsSeries = z.infer<typeof BollingerBandsSeriesSchema>;
|
|
51
|
+
export type SmaSeriesFixed = z.infer<typeof SmaSeriesFixedSchema>;
|
|
52
|
+
export type EmaSeriesFixed = z.infer<typeof EmaSeriesFixedSchema>;
|
|
53
|
+
export type ChartIndicators = z.infer<typeof ChartIndicatorsSchema>;
|
|
54
|
+
export type ChartMeta = z.infer<typeof ChartMetaSchema>;
|
|
55
|
+
export type ChartPayload = z.infer<typeof ChartPayloadSchema>;
|
|
56
|
+
export type IndicatorsInternal = z.infer<typeof IndicatorsInternalSchema>;
|
|
57
|
+
|
|
58
|
+
// --- Tool DTOs ---
|
|
59
|
+
export type GetIndicatorsData = z.infer<typeof GetIndicatorsDataSchema>;
|
|
60
|
+
export type GetIndicatorsMeta = z.infer<typeof GetIndicatorsMetaSchema>;
|
|
61
|
+
export type GetTickerData = z.infer<typeof GetTickerDataSchemaOut>;
|
|
62
|
+
export type GetTickerMeta = z.infer<typeof GetTickerMetaSchemaOut>;
|
|
63
|
+
export type GetOrderbookData = z.infer<typeof GetOrderbookDataSchemaOut>;
|
|
64
|
+
export type GetOrderbookMeta = z.infer<typeof GetOrderbookMetaSchemaOut>;
|
|
65
|
+
export type GetCandlesData = z.infer<typeof GetCandlesDataSchemaOut>;
|
|
66
|
+
export type GetCandlesMeta = z.infer<typeof GetCandlesMetaSchemaOut>;
|
|
67
|
+
export type KeyPoint = z.infer<typeof KeyPointSchema>;
|
|
68
|
+
export type KeyPoints = z.infer<typeof KeyPointsSchema>;
|
|
69
|
+
export type VolumeStats = z.infer<typeof VolumeStatsSchema>;
|
|
70
|
+
|
|
71
|
+
// --- Chart / render options ---
|
|
72
|
+
export type BbMode = 'default' | 'extended';
|
|
73
|
+
export type IchimokuMode = 'default' | 'extended';
|
|
74
|
+
export type ChartStyle = 'candles' | 'line' | 'depth';
|
|
75
|
+
export type IchimokuOptions = { mode?: IchimokuMode };
|
|
76
|
+
export type RenderChartSvgOptions = z.input<typeof RenderChartSvgInputSchema>;
|
|
77
|
+
|
|
78
|
+
// --- Result pattern ---
|
|
79
|
+
export interface OkResult<T = Record<string, unknown>, M = Record<string, unknown>> {
|
|
80
|
+
ok: true;
|
|
81
|
+
summary: string;
|
|
82
|
+
data: T;
|
|
83
|
+
meta: M;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface FailResult<M = Record<string, unknown>> {
|
|
87
|
+
ok: false;
|
|
88
|
+
summary: string;
|
|
89
|
+
data: Record<string, unknown>;
|
|
90
|
+
meta: { errorType: string } & M;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type Result<T = unknown, M = unknown> = OkResult<T, M> | FailResult<M>;
|
|
94
|
+
|
|
95
|
+
// --- Candle validation ---
|
|
96
|
+
export type ValidateCandleDataData = z.infer<typeof ValidateCandleDataDataSchema>;
|
|
97
|
+
export type ValidateCandleDataMeta = z.infer<typeof ValidateCandleDataMetaSchema>;
|
package/src/schemas.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Auto-typed facade for schemas.js so that schemas.js is the single source of truth.
|
|
2
|
+
// Keep declarations minimal to avoid drift; use z.infer for consumer types.
|
|
3
|
+
import type { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
export declare const GetTickerInputSchema: z.ZodObject<z.ZodRawShape>;
|
|
6
|
+
export declare const GetOrderbookInputSchema: z.ZodObject<z.ZodRawShape>;
|
|
7
|
+
export declare const GetCandlesInputSchema: z.ZodObject<z.ZodRawShape>;
|
|
8
|
+
export declare const GetIndicatorsInputSchema: z.ZodObject<z.ZodRawShape>;
|
|
9
|
+
|
|
10
|
+
export declare const CandleTypeEnum: z.ZodTypeAny;
|
|
11
|
+
export declare const RenderChartSvgInputSchema: z.ZodObject<z.ZodRawShape>;
|
|
12
|
+
export declare const RenderChartSvgOutputSchema: z.ZodObject<z.ZodRawShape>;
|
|
13
|
+
|
|
14
|
+
export type RenderChartSvgInput = z.infer<typeof RenderChartSvgInputSchema>;
|
|
15
|
+
export type RenderChartSvgOutput = z.infer<typeof RenderChartSvgOutputSchema>;
|
|
16
|
+
export type GetTickerInput = z.infer<typeof GetTickerInputSchema>;
|
|
17
|
+
export type GetOrderbookInput = z.infer<typeof GetOrderbookInputSchema>;
|
|
18
|
+
export type GetCandlesInput = z.infer<typeof GetCandlesInputSchema>;
|
|
19
|
+
export type GetIndicatorsInput = z.infer<typeof GetIndicatorsInputSchema>;
|
|
20
|
+
|
|
21
|
+
export declare const NumericSeriesSchema: z.ZodTypeAny;
|
|
22
|
+
export declare const CandleSchema: z.ZodTypeAny;
|
|
23
|
+
export declare const IchimokuSeriesSchema: z.ZodTypeAny;
|
|
24
|
+
export declare const BollingerBandsSeriesSchema: z.ZodTypeAny;
|
|
25
|
+
export declare const SmaSeriesFixedSchema: z.ZodTypeAny;
|
|
26
|
+
export declare const ChartIndicatorsSchema: z.ZodTypeAny;
|
|
27
|
+
export declare const ChartMetaSchema: z.ZodTypeAny;
|
|
28
|
+
export declare const ChartStatsSchema: z.ZodTypeAny;
|
|
29
|
+
export declare const ChartPayloadSchema: z.ZodTypeAny;
|
|
30
|
+
export declare const TrendLabelEnum: z.ZodTypeAny;
|
|
31
|
+
export declare const IndicatorsInternalSchema: z.ZodTypeAny;
|
|
32
|
+
export declare const GetIndicatorsDataSchema: z.ZodTypeAny;
|
|
33
|
+
export declare const GetIndicatorsMetaSchema: z.ZodTypeAny;
|
|
34
|
+
export declare const GetTickerOutputSchema: z.ZodTypeAny;
|
|
35
|
+
export declare const GetOrderbookOutputSchema: z.ZodTypeAny;
|
|
36
|
+
export declare const GetCandlesOutputSchema: z.ZodTypeAny;
|
|
37
|
+
export declare const GetIndicatorsOutputSchema: z.ZodTypeAny;
|
package/src/schemas.ts
ADDED
package/src/server.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import './env.js'; // must be first — loads .env before other modules read process.env
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { getErrorMessage } from '../lib/error.js';
|
|
8
|
+
import { logError, logToolRun } from '../lib/logger.js';
|
|
9
|
+
import { type PromptDef, prompts as promptDefs } from './prompts.js';
|
|
10
|
+
import { appResourceRegistry } from './resources/app-resources.js';
|
|
11
|
+
import { allToolDefs } from './tool-registry.js';
|
|
12
|
+
|
|
13
|
+
const server = new McpServer({ name: 'bitbank-mcp', version: '0.4.2' });
|
|
14
|
+
// Explicit registries for tools/prompts to improve STDIO inspector compatibility
|
|
15
|
+
const registeredTools: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: Record<string, unknown>;
|
|
19
|
+
_meta?: Record<string, unknown>;
|
|
20
|
+
}> = [];
|
|
21
|
+
const registeredPrompts: Array<{ name: string; description: string }> = [];
|
|
22
|
+
|
|
23
|
+
type TextContent = { type: 'text'; text: string; _meta?: Record<string, unknown> };
|
|
24
|
+
type ToolReturn = { content: TextContent[]; structuredContent?: Record<string, unknown> };
|
|
25
|
+
|
|
26
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const respond = (result: unknown): ToolReturn => {
|
|
31
|
+
// 優先順位: custom content > summary > safe JSON fallback
|
|
32
|
+
let text = '';
|
|
33
|
+
if (isPlainObject(result)) {
|
|
34
|
+
const r = result;
|
|
35
|
+
// ツールが content を提供している場合(配列 or 文字列)を優先
|
|
36
|
+
if (Array.isArray(r.content)) {
|
|
37
|
+
const first = (r.content as unknown[]).find(
|
|
38
|
+
(c): c is { type: 'text'; text: string } => isPlainObject(c) && c.type === 'text' && typeof c.text === 'string',
|
|
39
|
+
);
|
|
40
|
+
if (first) {
|
|
41
|
+
text = first.text;
|
|
42
|
+
}
|
|
43
|
+
} else if (typeof r.content === 'string') {
|
|
44
|
+
text = r.content;
|
|
45
|
+
}
|
|
46
|
+
// 上記で未決定なら summary を採用
|
|
47
|
+
if (!text && typeof r.summary === 'string') {
|
|
48
|
+
text = r.summary;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// それでも空の場合は安全な短縮JSONにフォールバック
|
|
52
|
+
if (!text) {
|
|
53
|
+
try {
|
|
54
|
+
const json = JSON.stringify(
|
|
55
|
+
result,
|
|
56
|
+
(_key, value) => {
|
|
57
|
+
if (typeof value === 'string' && value.length > 2000) return `…omitted (${value.length} chars)`;
|
|
58
|
+
return value;
|
|
59
|
+
},
|
|
60
|
+
2,
|
|
61
|
+
);
|
|
62
|
+
text = json.length > 4000 ? `${json.slice(0, 4000)}\n…(truncated)…` : json;
|
|
63
|
+
} catch {
|
|
64
|
+
text = String(result);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// handler が McpResponse shape (`{ content, structuredContent: Result }`) を返している場合、
|
|
68
|
+
// 内側の structuredContent をそのまま採用する(二重ネストを防ぐ)。
|
|
69
|
+
// MCP Apps (SEP-1865) の iframe は `structuredContent` を直接参照するため、
|
|
70
|
+
// `{ structuredContent: { content, structuredContent: Result } }` のように包んでしまうと
|
|
71
|
+
// クライアント側で Result を取り出せない。
|
|
72
|
+
// Result shape (`{ ok, summary, data, meta }`) を直接返している場合は result 自体を採用する。
|
|
73
|
+
const structured = isPlainObject(result)
|
|
74
|
+
? isPlainObject(result.structuredContent)
|
|
75
|
+
? result.structuredContent
|
|
76
|
+
: result
|
|
77
|
+
: undefined;
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text }],
|
|
80
|
+
...(structured ? { structuredContent: structured } : {}),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Zod スキーマ → MCP inspector 用 JSON Schema に変換する。
|
|
85
|
+
* Zod 4 組み込みの toJSONSchema を使い、MCP 不要の additionalProperties を除去する。 */
|
|
86
|
+
function zodToInputJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
|
|
87
|
+
const json = z.toJSONSchema(schema, { target: 'openApi3' }) as Record<string, unknown>;
|
|
88
|
+
delete json.additionalProperties;
|
|
89
|
+
// default 付きフィールドは required から除外(Zod が parse 時に適用するため MCP 入力では不要)
|
|
90
|
+
const props = json.properties as Record<string, Record<string, unknown>> | undefined;
|
|
91
|
+
if (Array.isArray(json.required) && props) {
|
|
92
|
+
json.required = (json.required as string[]).filter((k) => !('default' in (props[k] ?? {})));
|
|
93
|
+
if ((json.required as string[]).length === 0) delete json.required;
|
|
94
|
+
}
|
|
95
|
+
return json;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** SDK の registerTool に渡すための ZodRawShape を取得する。
|
|
99
|
+
* .describe() / .default() / .optional() 等のラッパーを再帰的にアンラップする。 */
|
|
100
|
+
function getRawShape(s: z.ZodTypeAny): z.ZodRawShape {
|
|
101
|
+
type Unwrappable = {
|
|
102
|
+
shape?: z.ZodRawShape;
|
|
103
|
+
_def?: { schema?: z.ZodTypeAny; innerType?: z.ZodTypeAny; in?: z.ZodTypeAny };
|
|
104
|
+
};
|
|
105
|
+
let cur = s as Unwrappable;
|
|
106
|
+
for (let i = 0; i < 6; i++) {
|
|
107
|
+
if (cur.shape) break;
|
|
108
|
+
const def = cur._def;
|
|
109
|
+
if (!def) break;
|
|
110
|
+
// Zod 3: ZodEffects uses _def.schema / Zod 4: uses _def.in
|
|
111
|
+
if (def.schema) {
|
|
112
|
+
cur = def.schema as Unwrappable;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (def.in) {
|
|
116
|
+
cur = def.in as Unwrappable;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (def.innerType) {
|
|
120
|
+
cur = def.innerType as Unwrappable;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if (cur.shape) return cur.shape;
|
|
126
|
+
throw new Error('inputSchema must be or wrap a ZodObject');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function registerToolWithLog(
|
|
130
|
+
name: string,
|
|
131
|
+
schema: { description: string; inputSchema: z.ZodTypeAny; _meta?: Record<string, unknown> },
|
|
132
|
+
handler: (input: Record<string, unknown>, extra?: Record<string, unknown>) => Promise<unknown>,
|
|
133
|
+
) {
|
|
134
|
+
// Build JSON Schema for listing
|
|
135
|
+
const inputSchemaJson = zodToInputJsonSchema(schema.inputSchema);
|
|
136
|
+
registeredTools.push({
|
|
137
|
+
name,
|
|
138
|
+
description: schema.description,
|
|
139
|
+
inputSchema: inputSchemaJson,
|
|
140
|
+
...(schema._meta ? { _meta: schema._meta } : {}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// SDK の registerTool は第2引数に { inputSchema: ZodRawShape } を要求するが
|
|
144
|
+
// 型定義が厳密すぎて直接渡せないため、ここでキャストを集約する
|
|
145
|
+
const toolConfig: Record<string, unknown> = {
|
|
146
|
+
description: schema.description,
|
|
147
|
+
inputSchema: getRawShape(schema.inputSchema),
|
|
148
|
+
};
|
|
149
|
+
if (schema._meta) toolConfig._meta = schema._meta;
|
|
150
|
+
(server as unknown as { registerTool: (n: string, s: unknown, h: unknown) => void }).registerTool(
|
|
151
|
+
name,
|
|
152
|
+
toolConfig,
|
|
153
|
+
async (input: Record<string, unknown>, extra?: Record<string, unknown>) => {
|
|
154
|
+
const TOOL_TIMEOUT_MS = 60_000;
|
|
155
|
+
const t0 = Date.now();
|
|
156
|
+
try {
|
|
157
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
158
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
159
|
+
timeoutId = setTimeout(
|
|
160
|
+
() => reject(new Error(`ツール実行がタイムアウトしました (${TOOL_TIMEOUT_MS / 1000}秒)`)),
|
|
161
|
+
TOOL_TIMEOUT_MS,
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
// elicitation 等で server.elicitInput / getClientCapabilities を使うツール向けに、
|
|
165
|
+
// SDK から渡される extra に McpServer インスタンスを合流させる。
|
|
166
|
+
const handlerExtra = { ...extra, server };
|
|
167
|
+
const result = await Promise.race([handler(input, handlerExtra), timeoutPromise]).finally(() => {
|
|
168
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
169
|
+
});
|
|
170
|
+
const ms = Date.now() - t0;
|
|
171
|
+
logToolRun({ tool: name, input, result, ms });
|
|
172
|
+
return respond(result);
|
|
173
|
+
} catch (err: unknown) {
|
|
174
|
+
const ms = Date.now() - t0;
|
|
175
|
+
logError(name, err, input);
|
|
176
|
+
const message = getErrorMessage(err);
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: 'text', text: `内部エラー: ${message || '不明なエラー'}` }],
|
|
179
|
+
structuredContent: {
|
|
180
|
+
ok: false,
|
|
181
|
+
summary: `内部エラー: ${message || '不明なエラー'}`,
|
|
182
|
+
meta: { ms, errorType: 'internal' },
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// === Auto-register all tools from registry ===
|
|
191
|
+
for (const def of allToolDefs) {
|
|
192
|
+
registerToolWithLog(
|
|
193
|
+
def.name,
|
|
194
|
+
{ description: def.description, inputSchema: def.inputSchema, ...(def._meta ? { _meta: def._meta } : {}) },
|
|
195
|
+
def.handler,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// === Register prompts (SDK 形式に寄せた最小導入) ===
|
|
200
|
+
|
|
201
|
+
type PromptMessage = PromptDef['messages'][number];
|
|
202
|
+
type ContentBlock = PromptMessage['content'][number];
|
|
203
|
+
|
|
204
|
+
function toSdkMessages(msgs: PromptMessage[]) {
|
|
205
|
+
return msgs.map((msg) => {
|
|
206
|
+
const blocks: ContentBlock[] = Array.isArray(msg.content) ? msg.content : [];
|
|
207
|
+
const text = blocks
|
|
208
|
+
.map((b) => {
|
|
209
|
+
if (b.type === 'text' && typeof b.text === 'string') return b.text;
|
|
210
|
+
// tool_code ブロック: PromptDef の型定義外だが実データに存在する
|
|
211
|
+
if (b.type === 'tool_code') {
|
|
212
|
+
const tc = b as unknown as { tool_name?: string; tool_input?: unknown };
|
|
213
|
+
const tool = tc.tool_name || 'tool';
|
|
214
|
+
const args = tc.tool_input ? JSON.stringify(tc.tool_input) : '{}';
|
|
215
|
+
return `Call ${tool} with ${args}`;
|
|
216
|
+
}
|
|
217
|
+
return '';
|
|
218
|
+
})
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
.join('\n');
|
|
221
|
+
return {
|
|
222
|
+
role: msg.role === 'assistant' ? ('assistant' as const) : ('user' as const),
|
|
223
|
+
content: { type: 'text' as const, text },
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function registerPromptSafe(name: string, def: Pick<PromptDef, 'description' | 'messages'>) {
|
|
229
|
+
const s = server as unknown as {
|
|
230
|
+
registerPrompt?: (name: string, meta: { description: string }, cb: () => unknown) => void;
|
|
231
|
+
};
|
|
232
|
+
if (typeof s.registerPrompt === 'function') {
|
|
233
|
+
registeredPrompts.push({ name, description: def.description });
|
|
234
|
+
s.registerPrompt(name, { description: def.description }, () => ({
|
|
235
|
+
description: def.description,
|
|
236
|
+
messages: toSdkMessages(def.messages),
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// === Register prompts from src/prompts.ts ===
|
|
242
|
+
for (const p of promptDefs) {
|
|
243
|
+
registerPromptSafe(p.name, p);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// === Register MCP Apps UI resources ===
|
|
247
|
+
// SDK の `registerResource` を使うことで `resources/list` と `resources/read` の
|
|
248
|
+
// JSON-RPC ルーティングが SDK 内部で正しく行われる。
|
|
249
|
+
// (以前の `setHandler('resources/...')` は SDK が要求する Zod スキーマではなく
|
|
250
|
+
// 文字列を渡していたため silently no-op となり、本番で `Method not found` を返していた)
|
|
251
|
+
for (const r of appResourceRegistry) {
|
|
252
|
+
const config: Record<string, unknown> = {
|
|
253
|
+
description: r.description,
|
|
254
|
+
mimeType: r.mimeType,
|
|
255
|
+
...(r.listMeta ? { _meta: r.listMeta } : {}),
|
|
256
|
+
};
|
|
257
|
+
(
|
|
258
|
+
server as unknown as {
|
|
259
|
+
registerResource: (
|
|
260
|
+
name: string,
|
|
261
|
+
uri: string,
|
|
262
|
+
config: Record<string, unknown>,
|
|
263
|
+
cb: (uri: URL) => Promise<unknown> | unknown,
|
|
264
|
+
) => void;
|
|
265
|
+
}
|
|
266
|
+
).registerResource(r.name, r.uri, config, async () => ({
|
|
267
|
+
contents: [
|
|
268
|
+
{
|
|
269
|
+
uri: r.uri,
|
|
270
|
+
mimeType: r.mimeType,
|
|
271
|
+
text: await r.read(),
|
|
272
|
+
...(r.contentMeta ? { _meta: r.contentMeta } : {}),
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// === トランスポート接続 ===
|
|
279
|
+
// SDK の McpServer.connect() は 1:1 でトランスポートを結合する (SDK issue #961)。
|
|
280
|
+
// MCP_ENABLE_HTTP=1 + PORT が設定されている場合は HTTP を優先し、stdio は接続しない。
|
|
281
|
+
const enableHttp = process.env.MCP_ENABLE_HTTP === '1';
|
|
282
|
+
const httpPort = (() => {
|
|
283
|
+
const p = Number(process.env.PORT);
|
|
284
|
+
return Number.isFinite(p) && p > 0 ? p : NaN;
|
|
285
|
+
})();
|
|
286
|
+
const useHttp = enableHttp && Number.isFinite(httpPort);
|
|
287
|
+
|
|
288
|
+
if (!useHttp) {
|
|
289
|
+
const transport = new StdioServerTransport();
|
|
290
|
+
await server.connect(transport);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// SDK の McpServer は setRequestHandler を public 型として export していないため、
|
|
294
|
+
// 低レベル API アクセスのキャストをこのヘルパーに集約する。
|
|
295
|
+
type HandlerFn = (request: unknown) => Promise<unknown>;
|
|
296
|
+
function setHandler(method: string, fn: HandlerFn) {
|
|
297
|
+
(server as unknown as { setRequestHandler?: (method: string, fn: HandlerFn) => void }).setRequestHandler?.(
|
|
298
|
+
method,
|
|
299
|
+
fn,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Fallback handlers to ensure list operations work over STDIO
|
|
304
|
+
try {
|
|
305
|
+
setHandler('tools/list', async () => ({
|
|
306
|
+
tools: registeredTools.map((t) => ({
|
|
307
|
+
name: t.name,
|
|
308
|
+
description: t.description,
|
|
309
|
+
inputSchema: t.inputSchema,
|
|
310
|
+
...(t._meta ? { _meta: t._meta } : {}),
|
|
311
|
+
})),
|
|
312
|
+
}));
|
|
313
|
+
setHandler('prompts/list', async () => ({
|
|
314
|
+
prompts: registeredPrompts.map((p) => ({ name: p.name, description: p.description })),
|
|
315
|
+
}));
|
|
316
|
+
// prompts/get: convert content arrays to single TextContent objects per MCP spec
|
|
317
|
+
setHandler('prompts/get', async (request: unknown) => {
|
|
318
|
+
try {
|
|
319
|
+
const params = (request as { params?: { name?: string } })?.params;
|
|
320
|
+
const name = params?.name;
|
|
321
|
+
console.error('[prompts/get] Requested name:', name);
|
|
322
|
+
if (!name) {
|
|
323
|
+
throw new Error('Prompt name is required');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const promptDef = promptDefs.find((p) => p.name === name);
|
|
327
|
+
if (!promptDef) {
|
|
328
|
+
console.error('[prompts/get] ERROR: Prompt not found:', name);
|
|
329
|
+
throw new Error(`Prompt not found: ${name}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.error('[prompts/get] Found prompt:', name, 'with', promptDef.messages.length, 'messages');
|
|
333
|
+
// prompts/get はテキストブロックのみ抽出(tool_code は除外)
|
|
334
|
+
const messages = promptDef.messages.map((msg) => {
|
|
335
|
+
const blocks: ContentBlock[] = Array.isArray(msg.content) ? msg.content : [];
|
|
336
|
+
const text = blocks
|
|
337
|
+
.filter((b): b is ContentBlock & { text: string } => b.type === 'text' && typeof b.text === 'string')
|
|
338
|
+
.map((b) => b.text)
|
|
339
|
+
.join('\n');
|
|
340
|
+
return {
|
|
341
|
+
role: msg.role === 'assistant' ? ('assistant' as const) : ('user' as const),
|
|
342
|
+
content: { type: 'text' as const, text },
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
return { description: promptDef.description, messages };
|
|
346
|
+
} catch (error: unknown) {
|
|
347
|
+
console.error('[prompts/get] EXCEPTION:', getErrorMessage(error));
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
} catch {}
|
|
352
|
+
|
|
353
|
+
// Optional HTTP transport (/mcp) when MCP_ENABLE_HTTP=1 + PORT
|
|
354
|
+
if (useHttp) {
|
|
355
|
+
try {
|
|
356
|
+
const { default: express } = await import('express');
|
|
357
|
+
const app = express();
|
|
358
|
+
app.use(express.json());
|
|
359
|
+
const allowedHosts = (process.env.ALLOWED_HOSTS || '127.0.0.1,localhost')
|
|
360
|
+
.split(',')
|
|
361
|
+
.map((s) => s.trim())
|
|
362
|
+
.filter(Boolean);
|
|
363
|
+
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
|
|
364
|
+
.split(',')
|
|
365
|
+
.map((s) => s.trim())
|
|
366
|
+
.filter(Boolean);
|
|
367
|
+
|
|
368
|
+
// StreamableHTTPServerTransport のコンストラクタ引数・戻り値が SDK で正確に export されていないため
|
|
369
|
+
// Transport 互換型にキャストを集約する
|
|
370
|
+
type Transport = Parameters<typeof server.connect>[0];
|
|
371
|
+
type HandleRequestFn = (
|
|
372
|
+
req: import('node:http').IncomingMessage,
|
|
373
|
+
res: import('node:http').ServerResponse,
|
|
374
|
+
body?: unknown,
|
|
375
|
+
) => Promise<void>;
|
|
376
|
+
const HttpTransport = StreamableHTTPServerTransport as unknown as new (
|
|
377
|
+
opts: Record<string, unknown>,
|
|
378
|
+
) => Transport & {
|
|
379
|
+
handleRequest?: HandleRequestFn;
|
|
380
|
+
};
|
|
381
|
+
const httpTransport = new HttpTransport({
|
|
382
|
+
path: '/mcp',
|
|
383
|
+
sessionIdGenerator: () => randomUUID(),
|
|
384
|
+
enableDnsRebindingProtection: true,
|
|
385
|
+
...(allowedHosts.length ? { allowedHosts } : {}),
|
|
386
|
+
...(allowedOrigins.length ? { allowedOrigins } : {}),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await server.connect(httpTransport);
|
|
390
|
+
|
|
391
|
+
// SDK 公式の handleRequest を使って HTTP リクエストを処理する
|
|
392
|
+
if (typeof httpTransport.handleRequest === 'function') {
|
|
393
|
+
const handle = httpTransport.handleRequest.bind(httpTransport);
|
|
394
|
+
app.use('/mcp', (req, res, next) => {
|
|
395
|
+
handle(req, res, req.body).catch(next);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
app.listen(httpPort, () => {
|
|
399
|
+
// no stdout/stderr output to avoid STDIO transport contamination
|
|
400
|
+
});
|
|
401
|
+
} catch (e) {
|
|
402
|
+
// eslint-disable-next-line no-console
|
|
403
|
+
console.warn('HTTP transport setup skipped:', getErrorMessage(e));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { Result } from './schemas.js';
|
|
3
|
+
|
|
4
|
+
/** SVG 等を直接返すハンドラ用の事前フォーマット済み MCP レスポンス */
|
|
5
|
+
export interface McpResponse {
|
|
6
|
+
content: Array<{ type: string; text: string }>;
|
|
7
|
+
structuredContent: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ハンドラに渡される MCP リクエストコンテキスト。
|
|
12
|
+
*
|
|
13
|
+
* elicitation/sampling 等のサーバー → クライアント呼び出しを行うツール用。
|
|
14
|
+
* SDK の `RequestHandlerExtra` をそのまま受け取れるよう構造的型で受ける。
|
|
15
|
+
* `server` プロパティは server.ts 側で `McpServer` を合流させて注入する。
|
|
16
|
+
*/
|
|
17
|
+
export interface ToolHandlerExtra {
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* MCP ツール定義。各ツールファイル(または src/handlers/)で `toolDef` として export する。
|
|
23
|
+
* server.ts は tool-registry.ts 経由でこの定義を自動収集し registerToolWithLog に渡す。
|
|
24
|
+
*
|
|
25
|
+
* ツール追加/改修時は toolDef を更新するだけで server.ts の変更は不要。
|
|
26
|
+
*/
|
|
27
|
+
export interface ToolDefinition {
|
|
28
|
+
/** MCP ツール名 (e.g. 'get_ticker') */
|
|
29
|
+
name: string;
|
|
30
|
+
/** ツール説明(LLM 向け) */
|
|
31
|
+
description: string;
|
|
32
|
+
/** Zod 入力スキーマ */
|
|
33
|
+
inputSchema: z.ZodTypeAny;
|
|
34
|
+
/**
|
|
35
|
+
* MCP ハンドラ(入力を受けて結果を返す)。respond() で自動ラップされる。
|
|
36
|
+
* 第2引数 `extra` は elicitation 等で SDK のサーバー機能にアクセスする必要があるツールのみ参照する。
|
|
37
|
+
*/
|
|
38
|
+
handler(args: Record<string, unknown>, extra?: ToolHandlerExtra): Promise<Result | McpResponse>;
|
|
39
|
+
/**
|
|
40
|
+
* MCP ツール メタデータ。MCP Apps (SEP-1865) の `_meta.ui.resourceUri` 等を保持する。
|
|
41
|
+
* 未対応ホストでは無視される(Progressive Enhancement)。
|
|
42
|
+
*/
|
|
43
|
+
_meta?: Record<string, unknown>;
|
|
44
|
+
}
|