@nahisaho/katashiro-mcp-server 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.
@@ -0,0 +1,438 @@
1
+ /**
2
+ * MCPProtocolHandler - MCP Protocol implementation
3
+ *
4
+ * Implements the Model Context Protocol JSON-RPC handlers
5
+ * Handles lifecycle, tools, resources, and prompts methods
6
+ *
7
+ * @module @nahisaho/katashiro-mcp-server
8
+ * @task TSK-061
9
+ */
10
+
11
+ import { isOk } from '@nahisaho/katashiro-core';
12
+ import type {
13
+ KatashiroMCPServer,
14
+ ServerCapabilities,
15
+ ServerInfo,
16
+ MCPResource,
17
+ } from '../server/mcp-server.js';
18
+ import {
19
+ type JsonRpcRequest,
20
+ type JsonRpcResponse,
21
+ type JsonRpcNotification,
22
+ JsonRpcErrorCode,
23
+ createSuccessResponse,
24
+ createErrorResponse,
25
+ } from '../transport/stdio-transport.js';
26
+
27
+ /**
28
+ * MCP Protocol version
29
+ */
30
+ export const MCP_PROTOCOL_VERSION = '2024-11-05';
31
+
32
+ /**
33
+ * Initialize request params
34
+ */
35
+ export interface InitializeParams {
36
+ protocolVersion: string;
37
+ capabilities: Record<string, unknown>;
38
+ clientInfo: {
39
+ name: string;
40
+ version: string;
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Initialize result
46
+ */
47
+ export interface InitializeResult {
48
+ protocolVersion: string;
49
+ capabilities: ServerCapabilities;
50
+ serverInfo: ServerInfo;
51
+ }
52
+
53
+ /**
54
+ * Tools/list result
55
+ */
56
+ export interface ToolsListResult {
57
+ tools: Array<{
58
+ name: string;
59
+ description: string;
60
+ inputSchema: unknown;
61
+ }>;
62
+ }
63
+
64
+ /**
65
+ * Tools/call params
66
+ */
67
+ export interface ToolsCallParams {
68
+ name: string;
69
+ arguments?: Record<string, unknown>;
70
+ }
71
+
72
+ /**
73
+ * Resources/list result
74
+ */
75
+ export interface ResourcesListResult {
76
+ resources: Array<{
77
+ uri: string;
78
+ name: string;
79
+ description?: string;
80
+ mimeType?: string;
81
+ }>;
82
+ }
83
+
84
+ /**
85
+ * Resources/read params
86
+ */
87
+ export interface ResourcesReadParams {
88
+ uri: string;
89
+ }
90
+
91
+ /**
92
+ * Prompts/list result
93
+ */
94
+ export interface PromptsListResult {
95
+ prompts: Array<{
96
+ name: string;
97
+ description: string;
98
+ arguments?: Array<{
99
+ name: string;
100
+ description: string;
101
+ required?: boolean;
102
+ }>;
103
+ }>;
104
+ }
105
+
106
+ /**
107
+ * Prompts/get params
108
+ */
109
+ export interface PromptsGetParams {
110
+ name: string;
111
+ arguments?: Record<string, unknown>;
112
+ }
113
+
114
+ /**
115
+ * Protocol state
116
+ */
117
+ export type ProtocolState = 'uninitialized' | 'initializing' | 'ready' | 'shutdown';
118
+
119
+ /**
120
+ * MCPProtocolHandler
121
+ *
122
+ * Handles MCP protocol messages and delegates to KatashiroMCPServer
123
+ */
124
+ export class MCPProtocolHandler {
125
+ private state: ProtocolState = 'uninitialized';
126
+ private clientInfo: { name: string; version: string } | null = null;
127
+
128
+ constructor(private readonly server: KatashiroMCPServer) {}
129
+
130
+ /**
131
+ * Get current protocol state
132
+ */
133
+ getState(): ProtocolState {
134
+ return this.state;
135
+ }
136
+
137
+ /**
138
+ * Get client info (after initialization)
139
+ */
140
+ getClientInfo(): { name: string; version: string } | null {
141
+ return this.clientInfo;
142
+ }
143
+
144
+ /**
145
+ * Handle incoming JSON-RPC message
146
+ */
147
+ async handleMessage(
148
+ message: JsonRpcRequest | JsonRpcNotification
149
+ ): Promise<JsonRpcResponse | void> {
150
+ const isRequest = 'id' in message;
151
+ const method = message.method;
152
+
153
+ // Handle notifications (no response expected)
154
+ if (!isRequest) {
155
+ await this.handleNotification(message as JsonRpcNotification);
156
+ return;
157
+ }
158
+
159
+ const request = message as JsonRpcRequest;
160
+
161
+ // Route based on method
162
+ try {
163
+ switch (method) {
164
+ case 'initialize':
165
+ return this.handleInitialize(request);
166
+
167
+ case 'ping':
168
+ return this.handlePing(request);
169
+
170
+ case 'tools/list':
171
+ return this.handleToolsList(request);
172
+
173
+ case 'tools/call':
174
+ return this.handleToolsCall(request);
175
+
176
+ case 'resources/list':
177
+ return this.handleResourcesList(request);
178
+
179
+ case 'resources/read':
180
+ return this.handleResourcesRead(request);
181
+
182
+ case 'prompts/list':
183
+ return this.handlePromptsList(request);
184
+
185
+ case 'prompts/get':
186
+ return this.handlePromptsGet(request);
187
+
188
+ default:
189
+ return createErrorResponse(
190
+ request.id,
191
+ JsonRpcErrorCode.MethodNotFound,
192
+ `Method not found: ${method}`
193
+ );
194
+ }
195
+ } catch (error) {
196
+ return createErrorResponse(
197
+ request.id,
198
+ JsonRpcErrorCode.InternalError,
199
+ error instanceof Error ? error.message : 'Internal error'
200
+ );
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Handle notifications
206
+ */
207
+ private async handleNotification(
208
+ notification: JsonRpcNotification
209
+ ): Promise<void> {
210
+ switch (notification.method) {
211
+ case 'notifications/initialized':
212
+ if (this.state === 'initializing') {
213
+ this.state = 'ready';
214
+ }
215
+ break;
216
+
217
+ case 'notifications/cancelled':
218
+ // Handle cancellation (not implemented yet)
219
+ break;
220
+
221
+ default:
222
+ // Unknown notification - ignore
223
+ break;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Handle initialize request
229
+ */
230
+ private handleInitialize(request: JsonRpcRequest): JsonRpcResponse {
231
+ if (this.state !== 'uninitialized') {
232
+ return createErrorResponse(
233
+ request.id,
234
+ JsonRpcErrorCode.InvalidRequest,
235
+ 'Already initialized'
236
+ );
237
+ }
238
+
239
+ const params = request.params as InitializeParams;
240
+ this.clientInfo = params.clientInfo;
241
+ this.state = 'initializing';
242
+
243
+ const result: InitializeResult = {
244
+ protocolVersion: MCP_PROTOCOL_VERSION,
245
+ capabilities: this.server.getCapabilities(),
246
+ serverInfo: this.server.getServerInfo(),
247
+ };
248
+
249
+ return createSuccessResponse(request.id, result);
250
+ }
251
+
252
+ /**
253
+ * Handle ping request
254
+ */
255
+ private handlePing(request: JsonRpcRequest): JsonRpcResponse {
256
+ return createSuccessResponse(request.id, {});
257
+ }
258
+
259
+ /**
260
+ * Handle tools/list request
261
+ */
262
+ private handleToolsList(request: JsonRpcRequest): JsonRpcResponse {
263
+ this.ensureReady();
264
+
265
+ const tools = this.server.getTools();
266
+ const toolsResult: ToolsListResult = {
267
+ tools: tools.map((tool) => ({
268
+ name: tool.name,
269
+ description: tool.description,
270
+ inputSchema: tool.inputSchema,
271
+ })),
272
+ };
273
+
274
+ return createSuccessResponse(request.id, toolsResult);
275
+ }
276
+
277
+ /**
278
+ * Handle tools/call request
279
+ */
280
+ private async handleToolsCall(
281
+ request: JsonRpcRequest
282
+ ): Promise<JsonRpcResponse> {
283
+ this.ensureReady();
284
+
285
+ const params = request.params as ToolsCallParams;
286
+ if (!params.name) {
287
+ return createErrorResponse(
288
+ request.id,
289
+ JsonRpcErrorCode.InvalidParams,
290
+ 'Tool name required'
291
+ );
292
+ }
293
+
294
+ const result = await this.server.executeTool(
295
+ params.name,
296
+ params.arguments || {}
297
+ );
298
+
299
+ if (isOk(result)) {
300
+ return createSuccessResponse(request.id, result.value);
301
+ } else {
302
+ return createErrorResponse(
303
+ request.id,
304
+ JsonRpcErrorCode.InternalError,
305
+ result.error.message
306
+ );
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Handle resources/list request
312
+ */
313
+ private async handleResourcesList(
314
+ request: JsonRpcRequest
315
+ ): Promise<JsonRpcResponse> {
316
+ this.ensureReady();
317
+
318
+ const result = await this.server.listResources();
319
+
320
+ if (isOk(result)) {
321
+ const resources: ResourcesListResult = {
322
+ resources: result.value.map((r: MCPResource) => ({
323
+ uri: r.uri,
324
+ name: r.name,
325
+ description: r.description,
326
+ mimeType: r.mimeType,
327
+ })),
328
+ };
329
+ return createSuccessResponse(request.id, resources);
330
+ } else {
331
+ return createErrorResponse(
332
+ request.id,
333
+ JsonRpcErrorCode.InternalError,
334
+ result.error.message
335
+ );
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Handle resources/read request
341
+ */
342
+ private async handleResourcesRead(
343
+ request: JsonRpcRequest
344
+ ): Promise<JsonRpcResponse> {
345
+ this.ensureReady();
346
+
347
+ const params = request.params as ResourcesReadParams;
348
+ if (!params.uri) {
349
+ return createErrorResponse(
350
+ request.id,
351
+ JsonRpcErrorCode.InvalidParams,
352
+ 'Resource URI required'
353
+ );
354
+ }
355
+
356
+ const result = await this.server.readResource(params.uri);
357
+
358
+ if (isOk(result)) {
359
+ return createSuccessResponse(request.id, {
360
+ contents: [
361
+ {
362
+ uri: params.uri,
363
+ mimeType: 'text/plain',
364
+ text: result.value,
365
+ },
366
+ ],
367
+ });
368
+ } else {
369
+ return createErrorResponse(
370
+ request.id,
371
+ JsonRpcErrorCode.InternalError,
372
+ result.error.message
373
+ );
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Handle prompts/list request
379
+ */
380
+ private handlePromptsList(request: JsonRpcRequest): JsonRpcResponse {
381
+ this.ensureReady();
382
+
383
+ const prompts = this.server.getPrompts();
384
+ const promptsResult: PromptsListResult = {
385
+ prompts: prompts.map((prompt) => ({
386
+ name: prompt.name,
387
+ description: prompt.description,
388
+ arguments: prompt.arguments,
389
+ })),
390
+ };
391
+
392
+ return createSuccessResponse(request.id, promptsResult);
393
+ }
394
+
395
+ /**
396
+ * Handle prompts/get request
397
+ */
398
+ private async handlePromptsGet(
399
+ request: JsonRpcRequest
400
+ ): Promise<JsonRpcResponse> {
401
+ this.ensureReady();
402
+
403
+ const params = request.params as PromptsGetParams;
404
+ if (!params.name) {
405
+ return createErrorResponse(
406
+ request.id,
407
+ JsonRpcErrorCode.InvalidParams,
408
+ 'Prompt name required'
409
+ );
410
+ }
411
+
412
+ const result = await this.server.executePrompt(
413
+ params.name,
414
+ params.arguments || {}
415
+ );
416
+
417
+ if (isOk(result)) {
418
+ return createSuccessResponse(request.id, result.value);
419
+ } else {
420
+ return createErrorResponse(
421
+ request.id,
422
+ JsonRpcErrorCode.InternalError,
423
+ result.error.message
424
+ );
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Ensure server is ready for operations
430
+ */
431
+ private ensureReady(): void {
432
+ // Allow operations in both initializing and ready states
433
+ // This matches MCP spec where operations can happen after initialize
434
+ if (this.state === 'uninitialized') {
435
+ throw new Error('Server not initialized');
436
+ }
437
+ }
438
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * ResourceManager - リソース管理
3
+ *
4
+ * MCPリソースの登録・読み取り・購読を管理
5
+ *
6
+ * @module @nahisaho/katashiro-mcp-server
7
+ * @task TSK-063
8
+ */
9
+
10
+ import { ok, err, type Result } from '@nahisaho/katashiro-core';
11
+
12
+ /**
13
+ * Resource content
14
+ */
15
+ export interface ResourceContent {
16
+ content: string;
17
+ mimeType?: string;
18
+ }
19
+
20
+ /**
21
+ * Content provider function
22
+ */
23
+ export type ContentProvider = () => Promise<ResourceContent>;
24
+
25
+ /**
26
+ * Resource definition
27
+ */
28
+ export interface ResourceDefinition {
29
+ uri: string;
30
+ name: string;
31
+ description?: string;
32
+ mimeType?: string;
33
+ content?: string;
34
+ provider?: ContentProvider;
35
+ }
36
+
37
+ /**
38
+ * Resource template
39
+ */
40
+ export interface ResourceTemplate {
41
+ uriTemplate: string;
42
+ name: string;
43
+ description?: string;
44
+ mimeType?: string;
45
+ }
46
+
47
+ /**
48
+ * Subscription callback
49
+ */
50
+ export type SubscriptionCallback = (uri: string, content: string) => void;
51
+
52
+ /**
53
+ * List options
54
+ */
55
+ export interface ListOptions {
56
+ prefix?: string;
57
+ }
58
+
59
+ /**
60
+ * ResourceManager
61
+ *
62
+ * Manages MCP resources and subscriptions
63
+ */
64
+ export class ResourceManager {
65
+ private resources: Map<string, ResourceDefinition> = new Map();
66
+ private subscriptions: Map<string, Map<string, SubscriptionCallback>> = new Map();
67
+ private subscriptionCounter = 0;
68
+
69
+ /**
70
+ * Register a resource
71
+ *
72
+ * @param resource - Resource to register
73
+ * @returns Result
74
+ */
75
+ register(resource: ResourceDefinition): Result<void, Error> {
76
+ try {
77
+ if (this.resources.has(resource.uri)) {
78
+ return err(new Error(`Resource already registered: ${resource.uri}`));
79
+ }
80
+ this.resources.set(resource.uri, resource);
81
+ return ok(undefined);
82
+ } catch (error) {
83
+ return err(error instanceof Error ? error : new Error(String(error)));
84
+ }
85
+ }
86
+
87
+ /**
88
+ * List all resources
89
+ *
90
+ * @param options - List options
91
+ * @returns Array of resources
92
+ */
93
+ list(options: ListOptions = {}): Result<ResourceDefinition[], Error> {
94
+ try {
95
+ let resources = Array.from(this.resources.values());
96
+
97
+ if (options.prefix) {
98
+ resources = resources.filter((r) => r.uri.startsWith(options.prefix!));
99
+ }
100
+
101
+ return ok(resources);
102
+ } catch (error) {
103
+ return err(error instanceof Error ? error : new Error(String(error)));
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Read a resource
109
+ *
110
+ * @param uri - Resource URI
111
+ * @returns Resource content
112
+ */
113
+ async read(uri: string): Promise<Result<ResourceContent, Error>> {
114
+ try {
115
+ const resource = this.resources.get(uri);
116
+ if (!resource) {
117
+ return err(new Error(`Resource not found: ${uri}`));
118
+ }
119
+
120
+ // Use provider if available
121
+ if (resource.provider) {
122
+ const content = await resource.provider();
123
+ return ok(content);
124
+ }
125
+
126
+ // Return static content
127
+ return ok({
128
+ content: resource.content ?? '',
129
+ mimeType: resource.mimeType,
130
+ });
131
+ } catch (error) {
132
+ return err(error instanceof Error ? error : new Error(String(error)));
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Subscribe to resource changes
138
+ *
139
+ * @param uri - Resource URI
140
+ * @param callback - Callback function
141
+ * @returns Subscription ID
142
+ */
143
+ subscribe(uri: string, callback: SubscriptionCallback): Result<string, Error> {
144
+ try {
145
+ if (!this.resources.has(uri)) {
146
+ return err(new Error(`Resource not found: ${uri}`));
147
+ }
148
+
149
+ if (!this.subscriptions.has(uri)) {
150
+ this.subscriptions.set(uri, new Map());
151
+ }
152
+
153
+ const subId = `sub-${++this.subscriptionCounter}`;
154
+ this.subscriptions.get(uri)!.set(subId, callback);
155
+
156
+ return ok(subId);
157
+ } catch (error) {
158
+ return err(error instanceof Error ? error : new Error(String(error)));
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Unsubscribe from resource changes
164
+ *
165
+ * @param subscriptionId - Subscription ID
166
+ * @returns Whether unsubscribed
167
+ */
168
+ unsubscribe(subscriptionId: string): Result<boolean, Error> {
169
+ try {
170
+ for (const subs of this.subscriptions.values()) {
171
+ if (subs.has(subscriptionId)) {
172
+ subs.delete(subscriptionId);
173
+ return ok(true);
174
+ }
175
+ }
176
+ return ok(false);
177
+ } catch (error) {
178
+ return err(error instanceof Error ? error : new Error(String(error)));
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Update resource content and notify subscribers
184
+ *
185
+ * @param uri - Resource URI
186
+ * @param content - New content
187
+ * @returns Result
188
+ */
189
+ async update(uri: string, content: string): Promise<Result<void, Error>> {
190
+ try {
191
+ const resource = this.resources.get(uri);
192
+ if (!resource) {
193
+ return err(new Error(`Resource not found: ${uri}`));
194
+ }
195
+
196
+ // Update content
197
+ resource.content = content;
198
+ this.resources.set(uri, resource);
199
+
200
+ // Notify subscribers
201
+ const subs = this.subscriptions.get(uri);
202
+ if (subs) {
203
+ for (const callback of subs.values()) {
204
+ callback(uri, content);
205
+ }
206
+ }
207
+
208
+ return ok(undefined);
209
+ } catch (error) {
210
+ return err(error instanceof Error ? error : new Error(String(error)));
211
+ }
212
+ }
213
+
214
+ /**
215
+ * List resource templates
216
+ *
217
+ * @returns Array of templates
218
+ */
219
+ listTemplates(): Result<ResourceTemplate[], Error> {
220
+ try {
221
+ // Built-in templates
222
+ const templates: ResourceTemplate[] = [
223
+ {
224
+ uriTemplate: 'katashiro://knowledge/{topic}',
225
+ name: 'Knowledge Topic',
226
+ description: 'Access knowledge graph by topic',
227
+ mimeType: 'application/json',
228
+ },
229
+ {
230
+ uriTemplate: 'katashiro://patterns/{type}',
231
+ name: 'Pattern Type',
232
+ description: 'Access patterns by type',
233
+ mimeType: 'application/json',
234
+ },
235
+ ];
236
+
237
+ return ok(templates);
238
+ } catch (error) {
239
+ return err(error instanceof Error ? error : new Error(String(error)));
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Unregister a resource
245
+ *
246
+ * @param uri - Resource URI
247
+ * @returns Whether unregistered
248
+ */
249
+ unregister(uri: string): Result<boolean, Error> {
250
+ try {
251
+ this.subscriptions.delete(uri);
252
+ return ok(this.resources.delete(uri));
253
+ } catch (error) {
254
+ return err(error instanceof Error ? error : new Error(String(error)));
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Clear all resources
260
+ */
261
+ clear(): void {
262
+ this.resources.clear();
263
+ this.subscriptions.clear();
264
+ }
265
+ }