@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,331 @@
1
+ /**
2
+ * Protocol Handler テスト
3
+ *
4
+ * @task TSK-061
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import { KatashiroMCPServer } from '../../src/server/mcp-server.js';
9
+ import {
10
+ MCPProtocolHandler,
11
+ MCP_PROTOCOL_VERSION,
12
+ } from '../../src/protocol/protocol-handler.js';
13
+ import type {
14
+ JsonRpcRequest,
15
+ JsonRpcNotification,
16
+ } from '../../src/transport/stdio-transport.js';
17
+
18
+ describe('MCPProtocolHandler', () => {
19
+ let server: KatashiroMCPServer;
20
+ let handler: MCPProtocolHandler;
21
+
22
+ beforeEach(() => {
23
+ server = new KatashiroMCPServer();
24
+ handler = new MCPProtocolHandler(server);
25
+ });
26
+
27
+ describe('initialization', () => {
28
+ it('should start in uninitialized state', () => {
29
+ expect(handler.getState()).toBe('uninitialized');
30
+ });
31
+
32
+ it('should have null client info before initialization', () => {
33
+ expect(handler.getClientInfo()).toBeNull();
34
+ });
35
+ });
36
+
37
+ describe('initialize method', () => {
38
+ it('should handle initialize request', async () => {
39
+ const request: JsonRpcRequest = {
40
+ jsonrpc: '2.0',
41
+ id: 1,
42
+ method: 'initialize',
43
+ params: {
44
+ protocolVersion: MCP_PROTOCOL_VERSION,
45
+ capabilities: {},
46
+ clientInfo: {
47
+ name: 'test-client',
48
+ version: '1.0.0',
49
+ },
50
+ },
51
+ };
52
+
53
+ const response = await handler.handleMessage(request);
54
+
55
+ expect(response).toBeDefined();
56
+ expect(response?.result).toBeDefined();
57
+
58
+ const result = response?.result as {
59
+ protocolVersion: string;
60
+ serverInfo: { name: string; version: string };
61
+ };
62
+ expect(result.protocolVersion).toBe(MCP_PROTOCOL_VERSION);
63
+ expect(result.serverInfo.name).toBe('katashiro');
64
+ expect(handler.getState()).toBe('initializing');
65
+ });
66
+
67
+ it('should store client info after initialization', async () => {
68
+ const request: JsonRpcRequest = {
69
+ jsonrpc: '2.0',
70
+ id: 1,
71
+ method: 'initialize',
72
+ params: {
73
+ protocolVersion: MCP_PROTOCOL_VERSION,
74
+ capabilities: {},
75
+ clientInfo: {
76
+ name: 'test-client',
77
+ version: '1.0.0',
78
+ },
79
+ },
80
+ };
81
+
82
+ await handler.handleMessage(request);
83
+
84
+ expect(handler.getClientInfo()).toEqual({
85
+ name: 'test-client',
86
+ version: '1.0.0',
87
+ });
88
+ });
89
+
90
+ it('should reject double initialization', async () => {
91
+ const request: JsonRpcRequest = {
92
+ jsonrpc: '2.0',
93
+ id: 1,
94
+ method: 'initialize',
95
+ params: {
96
+ protocolVersion: MCP_PROTOCOL_VERSION,
97
+ capabilities: {},
98
+ clientInfo: { name: 'test', version: '1.0' },
99
+ },
100
+ };
101
+
102
+ // First initialization
103
+ await handler.handleMessage(request);
104
+
105
+ // Second initialization should fail
106
+ const response2 = await handler.handleMessage({ ...request, id: 2 });
107
+
108
+ expect(response2?.error).toBeDefined();
109
+ });
110
+ });
111
+
112
+ describe('notifications', () => {
113
+ it('should handle initialized notification', async () => {
114
+ // First initialize
115
+ const initRequest: JsonRpcRequest = {
116
+ jsonrpc: '2.0',
117
+ id: 1,
118
+ method: 'initialize',
119
+ params: {
120
+ protocolVersion: MCP_PROTOCOL_VERSION,
121
+ capabilities: {},
122
+ clientInfo: { name: 'test', version: '1.0' },
123
+ },
124
+ };
125
+ await handler.handleMessage(initRequest);
126
+
127
+ // Then send initialized notification
128
+ const notification: JsonRpcNotification = {
129
+ jsonrpc: '2.0',
130
+ method: 'notifications/initialized',
131
+ };
132
+
133
+ const result = await handler.handleMessage(notification);
134
+
135
+ expect(result).toBeUndefined(); // Notifications don't return responses
136
+ expect(handler.getState()).toBe('ready');
137
+ });
138
+ });
139
+
140
+ describe('ping method', () => {
141
+ it('should respond to ping', async () => {
142
+ const request: JsonRpcRequest = {
143
+ jsonrpc: '2.0',
144
+ id: 1,
145
+ method: 'ping',
146
+ };
147
+
148
+ const response = await handler.handleMessage(request);
149
+
150
+ expect(response?.result).toEqual({});
151
+ });
152
+ });
153
+
154
+ describe('tools methods', () => {
155
+ beforeEach(async () => {
156
+ // Initialize first
157
+ await handler.handleMessage({
158
+ jsonrpc: '2.0',
159
+ id: 0,
160
+ method: 'initialize',
161
+ params: {
162
+ protocolVersion: MCP_PROTOCOL_VERSION,
163
+ capabilities: {},
164
+ clientInfo: { name: 'test', version: '1.0' },
165
+ },
166
+ });
167
+ });
168
+
169
+ it('should list tools', async () => {
170
+ const request: JsonRpcRequest = {
171
+ jsonrpc: '2.0',
172
+ id: 1,
173
+ method: 'tools/list',
174
+ };
175
+
176
+ const response = await handler.handleMessage(request);
177
+
178
+ expect(response?.result).toBeDefined();
179
+ const result = response?.result as { tools: unknown[] };
180
+ expect(Array.isArray(result.tools)).toBe(true);
181
+ expect(result.tools.length).toBeGreaterThan(0);
182
+ });
183
+
184
+ it('should call tool', async () => {
185
+ const request: JsonRpcRequest = {
186
+ jsonrpc: '2.0',
187
+ id: 1,
188
+ method: 'tools/call',
189
+ params: {
190
+ name: 'web_search',
191
+ arguments: { query: 'test' },
192
+ },
193
+ };
194
+
195
+ const response = await handler.handleMessage(request);
196
+
197
+ expect(response?.result).toBeDefined();
198
+ expect(response?.error).toBeUndefined();
199
+ });
200
+
201
+ it('should return error for missing tool name', async () => {
202
+ const request: JsonRpcRequest = {
203
+ jsonrpc: '2.0',
204
+ id: 1,
205
+ method: 'tools/call',
206
+ params: {},
207
+ };
208
+
209
+ const response = await handler.handleMessage(request);
210
+
211
+ expect(response?.error).toBeDefined();
212
+ expect(response?.error?.message).toContain('Tool name required');
213
+ });
214
+ });
215
+
216
+ describe('resources methods', () => {
217
+ beforeEach(async () => {
218
+ await handler.handleMessage({
219
+ jsonrpc: '2.0',
220
+ id: 0,
221
+ method: 'initialize',
222
+ params: {
223
+ protocolVersion: MCP_PROTOCOL_VERSION,
224
+ capabilities: {},
225
+ clientInfo: { name: 'test', version: '1.0' },
226
+ },
227
+ });
228
+ });
229
+
230
+ it('should list resources', async () => {
231
+ const request: JsonRpcRequest = {
232
+ jsonrpc: '2.0',
233
+ id: 1,
234
+ method: 'resources/list',
235
+ };
236
+
237
+ const response = await handler.handleMessage(request);
238
+
239
+ expect(response?.result).toBeDefined();
240
+ const result = response?.result as { resources: unknown[] };
241
+ expect(Array.isArray(result.resources)).toBe(true);
242
+ });
243
+
244
+ it('should read resource', async () => {
245
+ const request: JsonRpcRequest = {
246
+ jsonrpc: '2.0',
247
+ id: 1,
248
+ method: 'resources/read',
249
+ params: {
250
+ uri: 'katashiro://knowledge/graph',
251
+ },
252
+ };
253
+
254
+ const response = await handler.handleMessage(request);
255
+
256
+ expect(response?.result).toBeDefined();
257
+ });
258
+ });
259
+
260
+ describe('prompts methods', () => {
261
+ beforeEach(async () => {
262
+ await handler.handleMessage({
263
+ jsonrpc: '2.0',
264
+ id: 0,
265
+ method: 'initialize',
266
+ params: {
267
+ protocolVersion: MCP_PROTOCOL_VERSION,
268
+ capabilities: {},
269
+ clientInfo: { name: 'test', version: '1.0' },
270
+ },
271
+ });
272
+ });
273
+
274
+ it('should list prompts', async () => {
275
+ const request: JsonRpcRequest = {
276
+ jsonrpc: '2.0',
277
+ id: 1,
278
+ method: 'prompts/list',
279
+ };
280
+
281
+ const response = await handler.handleMessage(request);
282
+
283
+ expect(response?.result).toBeDefined();
284
+ const result = response?.result as { prompts: unknown[] };
285
+ expect(Array.isArray(result.prompts)).toBe(true);
286
+ });
287
+
288
+ it('should get prompt', async () => {
289
+ const request: JsonRpcRequest = {
290
+ jsonrpc: '2.0',
291
+ id: 1,
292
+ method: 'prompts/get',
293
+ params: {
294
+ name: 'research_topic',
295
+ arguments: { topic: 'AI' },
296
+ },
297
+ };
298
+
299
+ const response = await handler.handleMessage(request);
300
+
301
+ expect(response?.result).toBeDefined();
302
+ });
303
+ });
304
+
305
+ describe('error handling', () => {
306
+ it('should return method not found for unknown methods', async () => {
307
+ const request: JsonRpcRequest = {
308
+ jsonrpc: '2.0',
309
+ id: 1,
310
+ method: 'unknown/method',
311
+ };
312
+
313
+ const response = await handler.handleMessage(request);
314
+
315
+ expect(response?.error).toBeDefined();
316
+ expect(response?.error?.code).toBe(-32601); // Method not found
317
+ });
318
+
319
+ it('should reject operations before initialization', async () => {
320
+ const request: JsonRpcRequest = {
321
+ jsonrpc: '2.0',
322
+ id: 1,
323
+ method: 'tools/list',
324
+ };
325
+
326
+ const response = await handler.handleMessage(request);
327
+
328
+ expect(response?.error).toBeDefined();
329
+ });
330
+ });
331
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * ResourceManager テスト
3
+ *
4
+ * @task TSK-063
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import { ResourceManager } from '../../src/resources/resource-manager.js';
9
+ import { isOk } from '@nahisaho/katashiro-core';
10
+
11
+ describe('ResourceManager', () => {
12
+ let manager: ResourceManager;
13
+
14
+ beforeEach(() => {
15
+ manager = new ResourceManager();
16
+ });
17
+
18
+ describe('registration', () => {
19
+ it('should register a resource', () => {
20
+ const result = manager.register({
21
+ uri: 'katashiro://test/resource',
22
+ name: 'Test Resource',
23
+ description: 'A test resource',
24
+ mimeType: 'application/json',
25
+ });
26
+
27
+ expect(isOk(result)).toBe(true);
28
+ });
29
+
30
+ it('should register resource with content provider', () => {
31
+ const result = manager.register({
32
+ uri: 'katashiro://dynamic/data',
33
+ name: 'Dynamic Data',
34
+ provider: async () => ({ content: 'Dynamic content' }),
35
+ });
36
+
37
+ expect(isOk(result)).toBe(true);
38
+ });
39
+
40
+ it('should reject duplicate URI', () => {
41
+ manager.register({
42
+ uri: 'katashiro://test',
43
+ name: 'First',
44
+ });
45
+
46
+ const result = manager.register({
47
+ uri: 'katashiro://test',
48
+ name: 'Second',
49
+ });
50
+
51
+ expect(isOk(result)).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('listing', () => {
56
+ it('should list all resources', () => {
57
+ manager.register({ uri: 'katashiro://a', name: 'A' });
58
+ manager.register({ uri: 'katashiro://b', name: 'B' });
59
+
60
+ const result = manager.list();
61
+ expect(isOk(result)).toBe(true);
62
+ if (isOk(result)) {
63
+ expect(result.value).toHaveLength(2);
64
+ }
65
+ });
66
+
67
+ it('should filter by prefix', () => {
68
+ manager.register({ uri: 'katashiro://data/one', name: 'One' });
69
+ manager.register({ uri: 'katashiro://data/two', name: 'Two' });
70
+ manager.register({ uri: 'katashiro://other/three', name: 'Three' });
71
+
72
+ const result = manager.list({ prefix: 'katashiro://data/' });
73
+ expect(isOk(result)).toBe(true);
74
+ if (isOk(result)) {
75
+ expect(result.value).toHaveLength(2);
76
+ }
77
+ });
78
+ });
79
+
80
+ describe('reading', () => {
81
+ it('should read static content', async () => {
82
+ manager.register({
83
+ uri: 'katashiro://static',
84
+ name: 'Static',
85
+ content: 'Static content here',
86
+ });
87
+
88
+ const result = await manager.read('katashiro://static');
89
+ expect(isOk(result)).toBe(true);
90
+ if (isOk(result)) {
91
+ expect(result.value.content).toBe('Static content here');
92
+ }
93
+ });
94
+
95
+ it('should read dynamic content from provider', async () => {
96
+ manager.register({
97
+ uri: 'katashiro://dynamic',
98
+ name: 'Dynamic',
99
+ provider: async () => ({ content: 'Generated content' }),
100
+ });
101
+
102
+ const result = await manager.read('katashiro://dynamic');
103
+ expect(isOk(result)).toBe(true);
104
+ if (isOk(result)) {
105
+ expect(result.value.content).toBe('Generated content');
106
+ }
107
+ });
108
+
109
+ it('should return error for unknown resource', async () => {
110
+ const result = await manager.read('katashiro://unknown');
111
+ expect(isOk(result)).toBe(false);
112
+ });
113
+ });
114
+
115
+ describe('subscriptions', () => {
116
+ it('should subscribe to resource changes', () => {
117
+ manager.register({ uri: 'katashiro://watched', name: 'Watched' });
118
+
119
+ const result = manager.subscribe('katashiro://watched', () => {});
120
+ expect(isOk(result)).toBe(true);
121
+ if (isOk(result)) {
122
+ expect(result.value).toBeDefined(); // subscription id
123
+ }
124
+ });
125
+
126
+ it('should unsubscribe', () => {
127
+ manager.register({ uri: 'katashiro://watched', name: 'Watched' });
128
+ const subResult = manager.subscribe('katashiro://watched', () => {});
129
+
130
+ if (isOk(subResult)) {
131
+ const result = manager.unsubscribe(subResult.value);
132
+ expect(isOk(result)).toBe(true);
133
+ }
134
+ });
135
+
136
+ it('should notify subscribers on update', async () => {
137
+ let notified = false;
138
+ manager.register({
139
+ uri: 'katashiro://notify-test',
140
+ name: 'Notify Test',
141
+ content: 'Initial',
142
+ });
143
+
144
+ manager.subscribe('katashiro://notify-test', () => {
145
+ notified = true;
146
+ });
147
+
148
+ await manager.update('katashiro://notify-test', 'Updated');
149
+ expect(notified).toBe(true);
150
+ });
151
+ });
152
+
153
+ describe('templates', () => {
154
+ it('should list resource templates', () => {
155
+ const result = manager.listTemplates();
156
+ expect(isOk(result)).toBe(true);
157
+ if (isOk(result)) {
158
+ expect(Array.isArray(result.value)).toBe(true);
159
+ }
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,180 @@
1
+ /**
2
+ * STDIO Transport テスト
3
+ *
4
+ * @task TSK-061
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import { Readable, Writable } from 'stream';
9
+ import {
10
+ StdioTransport,
11
+ createSuccessResponse,
12
+ createErrorResponse,
13
+ JsonRpcErrorCode,
14
+ } from '../../src/transport/stdio-transport.js';
15
+
16
+ /**
17
+ * Create a mock readable stream
18
+ */
19
+ function createMockReadable(): Readable {
20
+ return new Readable({
21
+ read() {
22
+ // Do nothing - we'll push data manually
23
+ },
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Create a mock writable stream that captures output
29
+ */
30
+ function createMockWritable(): { writable: Writable; output: string[] } {
31
+ const output: string[] = [];
32
+ const writable = new Writable({
33
+ write(chunk, _encoding, callback) {
34
+ output.push(chunk.toString());
35
+ callback();
36
+ },
37
+ });
38
+ return { writable, output };
39
+ }
40
+
41
+ describe('StdioTransport', () => {
42
+ let transport: StdioTransport;
43
+ let mockInput: Readable;
44
+ let mockOutput: { writable: Writable; output: string[] };
45
+
46
+ beforeEach(() => {
47
+ mockInput = createMockReadable();
48
+ mockOutput = createMockWritable();
49
+ transport = new StdioTransport(mockInput, mockOutput.writable);
50
+ });
51
+
52
+ describe('initialization', () => {
53
+ it('should start in disconnected state', () => {
54
+ expect(transport.getState()).toBe('disconnected');
55
+ });
56
+
57
+ it('should transition to connected state on start', () => {
58
+ transport.start();
59
+ expect(transport.getState()).toBe('connected');
60
+ });
61
+
62
+ it('should transition to closed state on stop', () => {
63
+ transport.start();
64
+ transport.stop();
65
+ expect(transport.getState()).toBe('closed');
66
+ });
67
+ });
68
+
69
+ describe('message handling', () => {
70
+ it('should call message handler on valid JSON-RPC message', async () => {
71
+ let receivedMessage: unknown = null;
72
+
73
+ transport.onMessage(async (message) => {
74
+ receivedMessage = message;
75
+ return createSuccessResponse(1, { result: 'ok' });
76
+ });
77
+
78
+ transport.start();
79
+
80
+ const message = JSON.stringify({
81
+ jsonrpc: '2.0',
82
+ id: 1,
83
+ method: 'test',
84
+ params: {},
85
+ });
86
+
87
+ mockInput.push(message + '\n');
88
+
89
+ // Wait for async processing
90
+ await new Promise((resolve) => setTimeout(resolve, 10));
91
+
92
+ expect(receivedMessage).not.toBeNull();
93
+ expect((receivedMessage as { method: string }).method).toBe('test');
94
+ });
95
+
96
+ it('should handle notifications (no id)', async () => {
97
+ let receivedMessage: unknown = null;
98
+
99
+ transport.onMessage(async (message) => {
100
+ receivedMessage = message;
101
+ return;
102
+ });
103
+
104
+ transport.start();
105
+
106
+ const notification = JSON.stringify({
107
+ jsonrpc: '2.0',
108
+ method: 'notifications/test',
109
+ params: {},
110
+ });
111
+
112
+ mockInput.push(notification + '\n');
113
+
114
+ await new Promise((resolve) => setTimeout(resolve, 10));
115
+
116
+ expect(receivedMessage).not.toBeNull();
117
+ });
118
+ });
119
+
120
+ describe('message sending', () => {
121
+ it('should send response to output', () => {
122
+ transport.start();
123
+
124
+ const response = createSuccessResponse(1, { data: 'test' });
125
+ transport.send(response);
126
+
127
+ expect(mockOutput.output.length).toBe(1);
128
+ const sent = JSON.parse(mockOutput.output[0].trim());
129
+ expect(sent.jsonrpc).toBe('2.0');
130
+ expect(sent.id).toBe(1);
131
+ expect(sent.result).toEqual({ data: 'test' });
132
+ });
133
+
134
+ it('should throw if transport not connected', () => {
135
+ const response = createSuccessResponse(1, {});
136
+ expect(() => transport.send(response)).toThrow('Transport not connected');
137
+ });
138
+ });
139
+ });
140
+
141
+ describe('JSON-RPC Response Helpers', () => {
142
+ describe('createSuccessResponse', () => {
143
+ it('should create valid success response', () => {
144
+ const response = createSuccessResponse(1, { data: 'test' });
145
+ expect(response.jsonrpc).toBe('2.0');
146
+ expect(response.id).toBe(1);
147
+ expect(response.result).toEqual({ data: 'test' });
148
+ expect(response.error).toBeUndefined();
149
+ });
150
+
151
+ it('should handle string id', () => {
152
+ const response = createSuccessResponse('abc-123', { value: 42 });
153
+ expect(response.id).toBe('abc-123');
154
+ });
155
+ });
156
+
157
+ describe('createErrorResponse', () => {
158
+ it('should create valid error response', () => {
159
+ const response = createErrorResponse(
160
+ 1,
161
+ JsonRpcErrorCode.MethodNotFound,
162
+ 'Method not found'
163
+ );
164
+ expect(response.jsonrpc).toBe('2.0');
165
+ expect(response.id).toBe(1);
166
+ expect(response.error?.code).toBe(-32601);
167
+ expect(response.error?.message).toBe('Method not found');
168
+ });
169
+
170
+ it('should include optional data', () => {
171
+ const response = createErrorResponse(
172
+ 1,
173
+ JsonRpcErrorCode.InvalidParams,
174
+ 'Invalid params',
175
+ { field: 'name' }
176
+ );
177
+ expect(response.error?.data).toEqual({ field: 'name' });
178
+ });
179
+ });
180
+ });