@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.
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/prompts/prompt-registry.d.ts.map +1 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/protocol-handler.d.ts.map +1 -0
- package/dist/resources/resource-manager.d.ts.map +1 -0
- package/dist/server/mcp-server.d.ts.map +1 -0
- package/dist/tools/tool-registry.d.ts.map +1 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/stdio-transport.d.ts.map +1 -0
- package/package.json +40 -0
- package/src/cli.ts +64 -0
- package/src/index.ts +77 -0
- package/src/prompts/prompt-registry.ts +195 -0
- package/src/protocol/index.ts +5 -0
- package/src/protocol/protocol-handler.ts +438 -0
- package/src/resources/resource-manager.ts +265 -0
- package/src/server/mcp-server.ts +474 -0
- package/src/tools/tool-registry.ts +158 -0
- package/src/transport/index.ts +5 -0
- package/src/transport/stdio-transport.ts +272 -0
- package/tests/unit/mcp-server.test.ts +164 -0
- package/tests/unit/prompt-registry.test.ts +193 -0
- package/tests/unit/protocol-handler.test.ts +331 -0
- package/tests/unit/resource-manager.test.ts +162 -0
- package/tests/unit/stdio-transport.test.ts +180 -0
- package/tests/unit/tool-registry.test.ts +140 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -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
|
+
}
|