@363045841yyt/klinechart-ai-runtime 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/README.md +130 -0
- package/dist/createWithMcp.d.ts +9 -0
- package/dist/createWithMcp.d.ts.map +1 -0
- package/dist/createWithMcp.js +15 -0
- package/dist/createWithMcp.js.map +1 -0
- package/dist/describeControllers.d.ts +34 -0
- package/dist/describeControllers.d.ts.map +1 -0
- package/dist/describeControllers.js +104 -0
- package/dist/describeControllers.js.map +1 -0
- package/dist/executeTool.d.ts +12 -0
- package/dist/executeTool.d.ts.map +1 -0
- package/dist/executeTool.js +63 -0
- package/dist/executeTool.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mcpServer.d.ts +35 -0
- package/dist/mcpServer.d.ts.map +1 -0
- package/dist/mcpServer.js +189 -0
- package/dist/mcpServer.js.map +1 -0
- package/dist/serialization.d.ts +28 -0
- package/dist/serialization.d.ts.map +1 -0
- package/dist/serialization.js +53 -0
- package/dist/serialization.js.map +1 -0
- package/dist/sessionRegistry.d.ts +19 -0
- package/dist/sessionRegistry.d.ts.map +1 -0
- package/dist/sessionRegistry.js +41 -0
- package/dist/sessionRegistry.js.map +1 -0
- package/dist/toolSchemas.d.ts +14 -0
- package/dist/toolSchemas.d.ts.map +1 -0
- package/dist/toolSchemas.js +216 -0
- package/dist/toolSchemas.js.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/chartBridge.integration.test.ts +100 -0
- package/src/__tests__/describeControllers.test.ts +163 -0
- package/src/__tests__/executeTool.test.ts +187 -0
- package/src/__tests__/mcpServer.integration.test.ts +155 -0
- package/src/__tests__/mcpServer.test.ts +30 -0
- package/src/__tests__/serialization.test.ts +116 -0
- package/src/__tests__/sessionRegistry.test.ts +139 -0
- package/src/__tests__/toolSchemas.test.ts +149 -0
- package/src/createWithMcp.ts +28 -0
- package/src/describeControllers.ts +166 -0
- package/src/executeTool.ts +92 -0
- package/src/index.ts +38 -0
- package/src/mcpServer.ts +268 -0
- package/src/serialization.ts +88 -0
- package/src/sessionRegistry.ts +61 -0
- package/src/toolSchemas.ts +235 -0
- package/src/types.ts +52 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type * from './types'
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
ALL_TOOLS,
|
|
5
|
+
TOOL_GROUPS,
|
|
6
|
+
CHART_NAVIGATION_TOOLS,
|
|
7
|
+
INDICATOR_TOOLS,
|
|
8
|
+
ALERT_TOOLS,
|
|
9
|
+
REPLAY_TOOLS,
|
|
10
|
+
findTool,
|
|
11
|
+
} from './toolSchemas'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
describeVolumeProfileState,
|
|
15
|
+
describeAnchoredVwap,
|
|
16
|
+
describeFootprintLatestBar,
|
|
17
|
+
describeAlerts,
|
|
18
|
+
type VolumeProfileSnapshot,
|
|
19
|
+
type AnchoredVwapSeriesSnapshot,
|
|
20
|
+
type FootprintLatestBarSnapshot,
|
|
21
|
+
type AlertSnapshot,
|
|
22
|
+
} from './describeControllers'
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
serialize,
|
|
26
|
+
deserialize,
|
|
27
|
+
ChartSerializationError,
|
|
28
|
+
type ChartSnapshotInput,
|
|
29
|
+
} from './serialization'
|
|
30
|
+
|
|
31
|
+
export { executeTool, type ToolCall, type ToolResult } from './executeTool'
|
|
32
|
+
|
|
33
|
+
export { SessionRegistry, type SessionHandle } from './sessionRegistry'
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
createChartControllerWithMcp,
|
|
37
|
+
type CreateChartWithMcpOptions,
|
|
38
|
+
} from './createWithMcp'
|
package/src/mcpServer.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
|
+
import {
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js'
|
|
7
|
+
import { WebSocketServer, type WebSocket } from 'ws'
|
|
8
|
+
import type { ToolCall, ToolResult } from './executeTool'
|
|
9
|
+
import { ALL_TOOLS } from './toolSchemas'
|
|
10
|
+
import type { ControllerDescription } from './types'
|
|
11
|
+
import { SessionRegistry, type SessionHandle } from './sessionRegistry'
|
|
12
|
+
|
|
13
|
+
class WsSessionHandle implements SessionHandle {
|
|
14
|
+
readonly sessionId: string
|
|
15
|
+
private ws: WebSocket
|
|
16
|
+
private pending = new Map<
|
|
17
|
+
string,
|
|
18
|
+
{ resolve: (r: ToolResult) => void; reject: (e: Error) => void }
|
|
19
|
+
>()
|
|
20
|
+
private msgSeq = 0
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
sessionId: string,
|
|
24
|
+
ws: WebSocket,
|
|
25
|
+
) {
|
|
26
|
+
this.sessionId = sessionId
|
|
27
|
+
this.ws = ws
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async executeTool(call: ToolCall): Promise<ToolResult> {
|
|
31
|
+
const requestId = `${this.sessionId}:${++this.msgSeq}`
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
this.pending.set(requestId, { resolve, reject })
|
|
35
|
+
|
|
36
|
+
if (this.ws.readyState !== this.ws.OPEN) {
|
|
37
|
+
this.pending.delete(requestId)
|
|
38
|
+
reject(new Error('WebSocket is not open'))
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.ws.send(
|
|
43
|
+
JSON.stringify({ type: 'tool:call', requestId, call }),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
const p = this.pending.get(requestId)
|
|
48
|
+
if (p) {
|
|
49
|
+
this.pending.delete(requestId)
|
|
50
|
+
reject(new Error(`Tool call timed out: ${call.name}`))
|
|
51
|
+
}
|
|
52
|
+
}, 30_000)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
handleMessage(msg: Record<string, unknown>): void {
|
|
57
|
+
if (msg.type === 'tool:result') {
|
|
58
|
+
const requestId = msg.requestId as string
|
|
59
|
+
const pending = this.pending.get(requestId)
|
|
60
|
+
if (pending) {
|
|
61
|
+
this.pending.delete(requestId)
|
|
62
|
+
pending.resolve(msg.result as ToolResult)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isAlive(): boolean {
|
|
68
|
+
return this.ws.readyState === this.ws.OPEN
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type { WsSessionHandle }
|
|
73
|
+
|
|
74
|
+
interface ToolResponseContent {
|
|
75
|
+
type: 'text'
|
|
76
|
+
text: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface McpServerOptions {
|
|
80
|
+
serverInfo?: { name?: string; version?: string }
|
|
81
|
+
ws?: { port?: number; host?: string }
|
|
82
|
+
registry?: SessionRegistry
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface McpServerInstance {
|
|
86
|
+
server: Server
|
|
87
|
+
registry: SessionRegistry
|
|
88
|
+
wss: WebSocketServer
|
|
89
|
+
start(): Promise<void>
|
|
90
|
+
stop(): Promise<void>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createMcpServer(options: McpServerOptions = {}): McpServerInstance {
|
|
94
|
+
const registry = options.registry ?? new SessionRegistry()
|
|
95
|
+
const wsPort = options.ws?.port ?? 8080
|
|
96
|
+
const wsHost = options.ws?.host ?? '0.0.0.0'
|
|
97
|
+
|
|
98
|
+
const serverInfoName = options.serverInfo?.name ?? 'klinechart-ai-mcp'
|
|
99
|
+
const serverInfoVersion = options.serverInfo?.version ?? '0.0.0'
|
|
100
|
+
|
|
101
|
+
const server = new Server(
|
|
102
|
+
{
|
|
103
|
+
name: serverInfoName,
|
|
104
|
+
version: serverInfoVersion,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
capabilities: {
|
|
108
|
+
tools: {},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
114
|
+
tools: ALL_TOOLS.map((t) => ({
|
|
115
|
+
name: t.name,
|
|
116
|
+
description: t.description,
|
|
117
|
+
inputSchema: t.inputSchema,
|
|
118
|
+
})),
|
|
119
|
+
}))
|
|
120
|
+
|
|
121
|
+
server.setRequestHandler(
|
|
122
|
+
CallToolRequestSchema,
|
|
123
|
+
async (request: {
|
|
124
|
+
params: { name: string; arguments?: Record<string, unknown> }
|
|
125
|
+
}) => {
|
|
126
|
+
const { name, arguments: args } = request.params
|
|
127
|
+
const schema = ALL_TOOLS.find((t) => t.name === name)
|
|
128
|
+
|
|
129
|
+
if (!schema) {
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: 'text' as const,
|
|
134
|
+
text: JSON.stringify({
|
|
135
|
+
success: false,
|
|
136
|
+
error: `Unknown tool: ${name}`,
|
|
137
|
+
}),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
isError: true,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const sessions = registry.getActiveSessionIds()
|
|
145
|
+
if (sessions.length === 0) {
|
|
146
|
+
console.warn(`[MCP] CallTool "${name}" but no sessions registered`)
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: 'text' as const,
|
|
151
|
+
text: JSON.stringify({
|
|
152
|
+
success: false,
|
|
153
|
+
error: 'No browser chart session connected.',
|
|
154
|
+
}),
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
isError: true,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sessionId = sessions[0]!
|
|
162
|
+
const handle = registry.get(sessionId)
|
|
163
|
+
if (!handle) {
|
|
164
|
+
return {
|
|
165
|
+
content: [
|
|
166
|
+
{
|
|
167
|
+
type: 'text' as const,
|
|
168
|
+
text: JSON.stringify({
|
|
169
|
+
success: false,
|
|
170
|
+
error: `Session ${sessionId} not found.`,
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
isError: true,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const result = await handle.executeTool({
|
|
179
|
+
name,
|
|
180
|
+
input: args ?? {},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const summary = registry.getSummary(sessionId)
|
|
184
|
+
const texts: string[] = [JSON.stringify(result)]
|
|
185
|
+
if (summary) texts.push(`Chart state: ${summary}`)
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
content: texts.map(
|
|
189
|
+
(text): ToolResponseContent => ({ type: 'text' as const, text }),
|
|
190
|
+
),
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
const wss = new WebSocketServer({ port: wsPort, host: wsHost })
|
|
196
|
+
wss.on('error', (err: NodeJS.ErrnoException) => {
|
|
197
|
+
console.error(`[MCP] WebSocket server error: ${err.message}`)
|
|
198
|
+
if (err.code === 'EADDRINUSE') {
|
|
199
|
+
console.error(
|
|
200
|
+
`[MCP] Port ${wsPort} is already in use. Use a different port via WS_PORT env or ws.port option.`,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
wss.on('connection', (ws: WebSocket) => {
|
|
206
|
+
console.error(`[MCP] WS client connected`)
|
|
207
|
+
let handle: WsSessionHandle | null = null
|
|
208
|
+
|
|
209
|
+
ws.on('message', (raw: Buffer) => {
|
|
210
|
+
let msg: Record<string, unknown>
|
|
211
|
+
try {
|
|
212
|
+
msg = JSON.parse(raw.toString())
|
|
213
|
+
} catch {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (msg.type === 'register') {
|
|
218
|
+
const sessionId = (msg.sessionId as string) ?? crypto.randomUUID()
|
|
219
|
+
handle = new WsSessionHandle(sessionId, ws)
|
|
220
|
+
registry.register(sessionId, handle)
|
|
221
|
+
console.error(
|
|
222
|
+
`[MCP] Session registered: ${sessionId} (total=${registry.getActiveSessionIds().length})`,
|
|
223
|
+
)
|
|
224
|
+
ws.send(JSON.stringify({ type: 'registered', sessionId }))
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (handle) {
|
|
229
|
+
handle.handleMessage(msg)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (msg.type === 'state:update' && handle) {
|
|
233
|
+
registry.updateState(
|
|
234
|
+
handle.sessionId,
|
|
235
|
+
msg.descriptions as Record<string, ControllerDescription>,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
ws.on('close', () => {
|
|
241
|
+
if (handle) {
|
|
242
|
+
console.error(`[MCP] Session disconnected: ${handle.sessionId}`)
|
|
243
|
+
registry.unregister(handle.sessionId)
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
ws.on('error', () => {
|
|
248
|
+
if (handle) {
|
|
249
|
+
registry.unregister(handle.sessionId)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
async function start(): Promise<void> {
|
|
255
|
+
const transport = new StdioServerTransport()
|
|
256
|
+
await server.connect(transport)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function stop(): Promise<void> {
|
|
260
|
+
await server.close()
|
|
261
|
+
for (const ws of wss.clients) {
|
|
262
|
+
ws.terminate()
|
|
263
|
+
}
|
|
264
|
+
wss.close()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { server, registry, wss, start, stop }
|
|
268
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { SerializedChartState } from './types'
|
|
2
|
+
|
|
3
|
+
const SCHEMA_VERSION = 1 as const
|
|
4
|
+
|
|
5
|
+
export class ChartSerializationError extends Error {
|
|
6
|
+
readonly code: string
|
|
7
|
+
constructor(code: string, message: string) {
|
|
8
|
+
super(message)
|
|
9
|
+
this.name = 'ChartSerializationError'
|
|
10
|
+
this.code = code
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChartSnapshotInput {
|
|
15
|
+
label?: string
|
|
16
|
+
viewport?: { zoomLevel: number; visibleFrom: number; visibleTo: number }
|
|
17
|
+
theme?: 'light' | 'dark'
|
|
18
|
+
indicators?: ReadonlyArray<{
|
|
19
|
+
definitionId: string
|
|
20
|
+
params: Readonly<Record<string, number | string | boolean>>
|
|
21
|
+
}>
|
|
22
|
+
alerts?: ReadonlyArray<{
|
|
23
|
+
id: string
|
|
24
|
+
name: string
|
|
25
|
+
predicate: unknown
|
|
26
|
+
oneShot: boolean
|
|
27
|
+
cooldownMs?: number
|
|
28
|
+
}>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function serialize(
|
|
32
|
+
snapshot: ChartSnapshotInput,
|
|
33
|
+
): SerializedChartState {
|
|
34
|
+
const controllers: SerializedChartState['controllers'] = {}
|
|
35
|
+
if (snapshot.viewport !== undefined) controllers.viewport = snapshot.viewport
|
|
36
|
+
if (snapshot.theme !== undefined) controllers.theme = snapshot.theme
|
|
37
|
+
if (snapshot.indicators !== undefined)
|
|
38
|
+
controllers.indicators = snapshot.indicators
|
|
39
|
+
if (snapshot.alerts !== undefined) controllers.alerts = snapshot.alerts
|
|
40
|
+
const out: SerializedChartState = {
|
|
41
|
+
schemaVersion: SCHEMA_VERSION,
|
|
42
|
+
snapshotTakenAt: new Date().toISOString(),
|
|
43
|
+
controllers,
|
|
44
|
+
}
|
|
45
|
+
if (snapshot.label !== undefined) out.label = snapshot.label
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function deserialize(json: string): SerializedChartState {
|
|
50
|
+
let parsed: unknown
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(json)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new ChartSerializationError(
|
|
55
|
+
'INVALID_JSON',
|
|
56
|
+
`Could not parse SerializedChartState as JSON: ${(err as Error).message}`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
60
|
+
throw new ChartSerializationError(
|
|
61
|
+
'NOT_OBJECT',
|
|
62
|
+
'SerializedChartState root must be an object.',
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
const root = parsed as Partial<SerializedChartState>
|
|
66
|
+
if (root.schemaVersion !== SCHEMA_VERSION) {
|
|
67
|
+
throw new ChartSerializationError(
|
|
68
|
+
'SCHEMA_VERSION_MISMATCH',
|
|
69
|
+
`Expected schemaVersion ${SCHEMA_VERSION}, got ${String(root.schemaVersion)}.`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
typeof root.snapshotTakenAt !== 'string' ||
|
|
74
|
+
Number.isNaN(Date.parse(root.snapshotTakenAt))
|
|
75
|
+
) {
|
|
76
|
+
throw new ChartSerializationError(
|
|
77
|
+
'INVALID_TIMESTAMP',
|
|
78
|
+
'snapshotTakenAt must be an ISO 8601 string.',
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
if (typeof root.controllers !== 'object' || root.controllers === null) {
|
|
82
|
+
throw new ChartSerializationError(
|
|
83
|
+
'MISSING_CONTROLLERS',
|
|
84
|
+
'controllers object is required.',
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
return root as SerializedChartState
|
|
88
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ToolCall, ToolResult } from './executeTool'
|
|
2
|
+
import type { ControllerDescription } from './types'
|
|
3
|
+
|
|
4
|
+
export interface SessionHandle {
|
|
5
|
+
readonly sessionId: string
|
|
6
|
+
executeTool(call: ToolCall): Promise<ToolResult>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class SessionRegistry {
|
|
10
|
+
private sessions = new Map<string, SessionHandle>()
|
|
11
|
+
private states = new Map<string, Record<string, ControllerDescription>>()
|
|
12
|
+
|
|
13
|
+
register(sessionId: string, handle: SessionHandle): void {
|
|
14
|
+
this.sessions.set(sessionId, handle)
|
|
15
|
+
this.states.set(sessionId, {})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
unregister(sessionId: string): void {
|
|
19
|
+
this.sessions.delete(sessionId)
|
|
20
|
+
this.states.delete(sessionId)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get(sessionId: string): SessionHandle | undefined {
|
|
24
|
+
return this.sessions.get(sessionId)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
has(sessionId: string): boolean {
|
|
28
|
+
return this.sessions.has(sessionId)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getActiveSessionIds(): string[] {
|
|
32
|
+
return Array.from(this.sessions.keys())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
updateState(
|
|
36
|
+
sessionId: string,
|
|
37
|
+
descriptions: Record<string, ControllerDescription>,
|
|
38
|
+
): void {
|
|
39
|
+
const existing = this.states.get(sessionId)
|
|
40
|
+
if (existing) {
|
|
41
|
+
Object.assign(existing, descriptions)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getState(
|
|
46
|
+
sessionId: string,
|
|
47
|
+
): Record<string, ControllerDescription> | undefined {
|
|
48
|
+
return this.states.get(sessionId)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getSummary(sessionId: string): string {
|
|
52
|
+
const descriptions = this.states.get(sessionId)
|
|
53
|
+
if (!descriptions) return 'No state available.'
|
|
54
|
+
|
|
55
|
+
const parts: string[] = []
|
|
56
|
+
for (const desc of Object.values(descriptions)) {
|
|
57
|
+
parts.push(`[${desc.controllerId}] ${desc.summary}`)
|
|
58
|
+
}
|
|
59
|
+
return parts.length > 0 ? parts.join(' | ') : 'No controllers described.'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { McpToolSchema } from './types'
|
|
2
|
+
|
|
3
|
+
export const CHART_NAVIGATION_TOOLS: McpToolSchema[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'chart.zoomToLevel',
|
|
6
|
+
description:
|
|
7
|
+
'Zoom the chart to a specific discrete level (1 = most zoomed out, ' +
|
|
8
|
+
'higher numbers = more zoomed in). Use when the user says "zoom in", ' +
|
|
9
|
+
'"zoom out", "fit the chart", or asks for a specific zoom level.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
level: {
|
|
14
|
+
type: 'integer',
|
|
15
|
+
minimum: 1,
|
|
16
|
+
maximum: 20,
|
|
17
|
+
description: 'Discrete zoom level. Higher = more zoomed in.',
|
|
18
|
+
},
|
|
19
|
+
anchorX: {
|
|
20
|
+
type: 'number',
|
|
21
|
+
description: 'Optional X coordinate to keep stationary during zoom.',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ['level'],
|
|
25
|
+
},
|
|
26
|
+
safety: 'mutates-state',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'chart.setTheme',
|
|
30
|
+
description:
|
|
31
|
+
'Switch between light and dark theme. Use when the user asks for ' +
|
|
32
|
+
'"dark mode", "light mode", or expresses a theme preference.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
theme: { type: 'string', enum: ['light', 'dark'] },
|
|
37
|
+
},
|
|
38
|
+
required: ['theme'],
|
|
39
|
+
},
|
|
40
|
+
safety: 'mutates-state',
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
export const INDICATOR_TOOLS: McpToolSchema[] = [
|
|
45
|
+
{
|
|
46
|
+
name: 'indicators.add',
|
|
47
|
+
description:
|
|
48
|
+
'Add a technical indicator to the chart by its catalog id (e.g. "MA", ' +
|
|
49
|
+
'"BOLL", "MACD", "RSI"). Use when the user asks to "add a moving ' +
|
|
50
|
+
'average", "show MACD", "I want to see Bollinger Bands", etc.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
definitionId: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description:
|
|
57
|
+
'Catalog id of the indicator. Common: MA, EMA, BOLL, EXPMA, ' +
|
|
58
|
+
'MACD, RSI, KDJ, VOL, ATR, OBV.',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
required: ['definitionId'],
|
|
62
|
+
},
|
|
63
|
+
outputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
instanceId: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description:
|
|
69
|
+
'New instance id, or null if the indicator was already active.',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
safety: 'mutates-state',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'indicators.remove',
|
|
77
|
+
description: 'Remove an indicator instance by its instance id.',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
instanceId: { type: 'string' },
|
|
82
|
+
},
|
|
83
|
+
required: ['instanceId'],
|
|
84
|
+
},
|
|
85
|
+
safety: 'mutates-state',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'indicators.updateParams',
|
|
89
|
+
description:
|
|
90
|
+
'Change the parameters of an active indicator (e.g. MA period from ' +
|
|
91
|
+
'20 to 50, BOLL multiplier from 2 to 2.5).',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
instanceId: { type: 'string' },
|
|
96
|
+
params: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {},
|
|
99
|
+
additionalProperties: true,
|
|
100
|
+
description: 'Param key to value map (numbers, strings, booleans).',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ['instanceId', 'params'],
|
|
104
|
+
},
|
|
105
|
+
safety: 'mutates-state',
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
export const ALERT_TOOLS: McpToolSchema[] = [
|
|
110
|
+
{
|
|
111
|
+
name: 'alerts.addPriceCross',
|
|
112
|
+
description:
|
|
113
|
+
'Create a price-crossing alert. Use when the user says "alert me when ' +
|
|
114
|
+
'BTC crosses 100k", "tell me when price drops below X", "wake me up at Y".',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
id: { type: 'string', description: 'Unique alert id.' },
|
|
119
|
+
name: { type: 'string', description: 'Human-readable label.' },
|
|
120
|
+
price: { type: 'number' },
|
|
121
|
+
direction: { type: 'string', enum: ['up', 'down', 'any'] },
|
|
122
|
+
oneShot: {
|
|
123
|
+
type: 'boolean',
|
|
124
|
+
description: 'If true, fires once then auto-disables.',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ['id', 'name', 'price', 'direction', 'oneShot'],
|
|
128
|
+
},
|
|
129
|
+
safety: 'mutates-state',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'alerts.addIndicatorCross',
|
|
133
|
+
description:
|
|
134
|
+
'Alert when an indicator value crosses a threshold (e.g. RSI > 70, MACD ' +
|
|
135
|
+
'histogram goes positive).',
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
id: { type: 'string' },
|
|
140
|
+
name: { type: 'string' },
|
|
141
|
+
indicatorId: { type: 'string' },
|
|
142
|
+
threshold: { type: 'number' },
|
|
143
|
+
direction: { type: 'string', enum: ['up', 'down', 'any'] },
|
|
144
|
+
oneShot: { type: 'boolean' },
|
|
145
|
+
},
|
|
146
|
+
required: [
|
|
147
|
+
'id',
|
|
148
|
+
'name',
|
|
149
|
+
'indicatorId',
|
|
150
|
+
'threshold',
|
|
151
|
+
'direction',
|
|
152
|
+
'oneShot',
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
safety: 'mutates-state',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'alerts.remove',
|
|
159
|
+
description:
|
|
160
|
+
'Remove an existing alert rule by its id. Use when the user says ' +
|
|
161
|
+
'"cancel the BTC alert", "delete that alert", "I no longer need notification X".',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: { id: { type: 'string' } },
|
|
165
|
+
required: ['id'],
|
|
166
|
+
},
|
|
167
|
+
safety: 'mutates-state',
|
|
168
|
+
},
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
export const REPLAY_TOOLS: McpToolSchema[] = [
|
|
172
|
+
{
|
|
173
|
+
name: 'replay.seekTo',
|
|
174
|
+
description:
|
|
175
|
+
'Move the replay cursor to a specific bar index. Use when the user says ' +
|
|
176
|
+
'"go to bar X", "rewind to the start", "show me what happened at this point".',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
position: { type: 'number', description: 'Bar index (float).' },
|
|
181
|
+
},
|
|
182
|
+
required: ['position'],
|
|
183
|
+
},
|
|
184
|
+
safety: 'mutates-state',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'replay.play',
|
|
188
|
+
description:
|
|
189
|
+
'Start replay from the current cursor at the configured pacing and speed. ' +
|
|
190
|
+
'Use when the user says "play", "start replay", "go".',
|
|
191
|
+
inputSchema: { type: 'object', properties: {} },
|
|
192
|
+
safety: 'mutates-state',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'replay.pause',
|
|
196
|
+
description:
|
|
197
|
+
'Pause the replay at the current bar. Use when the user says "pause", ' +
|
|
198
|
+
'"stop", "hold on", or wants to inspect a specific bar.',
|
|
199
|
+
inputSchema: { type: 'object', properties: {} },
|
|
200
|
+
safety: 'mutates-state',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'replay.setSpeed',
|
|
204
|
+
description:
|
|
205
|
+
'Set replay speed multiplier (1.0 = real-time, 10.0 = 10x speed). ' +
|
|
206
|
+
'Use when the user says "faster", "slow down", "real-time".',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: { speed: { type: 'number', minimum: 0.01 } },
|
|
210
|
+
required: ['speed'],
|
|
211
|
+
},
|
|
212
|
+
safety: 'mutates-state',
|
|
213
|
+
},
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
export const ALL_TOOLS: ReadonlyArray<McpToolSchema> = [
|
|
217
|
+
...CHART_NAVIGATION_TOOLS,
|
|
218
|
+
...INDICATOR_TOOLS,
|
|
219
|
+
...ALERT_TOOLS,
|
|
220
|
+
...REPLAY_TOOLS,
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
export const TOOL_GROUPS = {
|
|
224
|
+
navigation: CHART_NAVIGATION_TOOLS,
|
|
225
|
+
indicators: INDICATOR_TOOLS,
|
|
226
|
+
alerts: ALERT_TOOLS,
|
|
227
|
+
replay: REPLAY_TOOLS,
|
|
228
|
+
} as const
|
|
229
|
+
|
|
230
|
+
export function findTool(name: string): McpToolSchema | null {
|
|
231
|
+
for (const tool of ALL_TOOLS) {
|
|
232
|
+
if (tool.name === name) return tool
|
|
233
|
+
}
|
|
234
|
+
return null
|
|
235
|
+
}
|