@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,272 @@
1
+ /**
2
+ * StdioTransport - STDIO based JSON-RPC transport for MCP
3
+ *
4
+ * MCP specification compliant transport layer
5
+ * Uses newline-delimited JSON (NDJSON) for message framing
6
+ *
7
+ * @module @nahisaho/katashiro-mcp-server
8
+ * @task TSK-061
9
+ */
10
+
11
+ import * as readline from 'readline';
12
+ import type { Readable, Writable } from 'stream';
13
+
14
+ /**
15
+ * JSON-RPC 2.0 Request
16
+ */
17
+ export interface JsonRpcRequest {
18
+ jsonrpc: '2.0';
19
+ id: string | number;
20
+ method: string;
21
+ params?: unknown;
22
+ }
23
+
24
+ /**
25
+ * JSON-RPC 2.0 Notification (no id)
26
+ */
27
+ export interface JsonRpcNotification {
28
+ jsonrpc: '2.0';
29
+ method: string;
30
+ params?: unknown;
31
+ }
32
+
33
+ /**
34
+ * JSON-RPC 2.0 Response
35
+ */
36
+ export interface JsonRpcResponse {
37
+ jsonrpc: '2.0';
38
+ id: string | number;
39
+ result?: unknown;
40
+ error?: JsonRpcError;
41
+ }
42
+
43
+ /**
44
+ * JSON-RPC 2.0 Error
45
+ */
46
+ export interface JsonRpcError {
47
+ code: number;
48
+ message: string;
49
+ data?: unknown;
50
+ }
51
+
52
+ /**
53
+ * Standard JSON-RPC error codes
54
+ */
55
+ export const JsonRpcErrorCode = {
56
+ ParseError: -32700,
57
+ InvalidRequest: -32600,
58
+ MethodNotFound: -32601,
59
+ InvalidParams: -32602,
60
+ InternalError: -32603,
61
+ } as const;
62
+
63
+ /**
64
+ * Message handler type
65
+ */
66
+ export type MessageHandler = (
67
+ message: JsonRpcRequest | JsonRpcNotification
68
+ ) => Promise<JsonRpcResponse | void>;
69
+
70
+ /**
71
+ * Transport state
72
+ */
73
+ export type TransportState = 'disconnected' | 'connected' | 'closed';
74
+
75
+ /**
76
+ * StdioTransport
77
+ *
78
+ * Handles STDIO communication for MCP servers
79
+ * Implements JSON-RPC 2.0 over newline-delimited JSON
80
+ */
81
+ export class StdioTransport {
82
+ private state: TransportState = 'disconnected';
83
+ private readline: readline.Interface | null = null;
84
+ private messageHandler: MessageHandler | null = null;
85
+ private errorHandler: ((error: Error) => void) | null = null;
86
+
87
+ constructor(
88
+ private readonly input: Readable = process.stdin,
89
+ private readonly output: Writable = process.stdout
90
+ ) {}
91
+
92
+ /**
93
+ * Get current state
94
+ */
95
+ getState(): TransportState {
96
+ return this.state;
97
+ }
98
+
99
+ /**
100
+ * Set message handler
101
+ */
102
+ onMessage(handler: MessageHandler): void {
103
+ this.messageHandler = handler;
104
+ }
105
+
106
+ /**
107
+ * Set error handler
108
+ */
109
+ onError(handler: (error: Error) => void): void {
110
+ this.errorHandler = handler;
111
+ }
112
+
113
+ /**
114
+ * Start the transport
115
+ */
116
+ start(): void {
117
+ if (this.state !== 'disconnected') {
118
+ return;
119
+ }
120
+
121
+ this.readline = readline.createInterface({
122
+ input: this.input,
123
+ terminal: false,
124
+ });
125
+
126
+ this.readline.on('line', (line) => {
127
+ this.handleLine(line);
128
+ });
129
+
130
+ this.readline.on('close', () => {
131
+ this.state = 'closed';
132
+ });
133
+
134
+ this.readline.on('error', (error) => {
135
+ if (this.errorHandler) {
136
+ this.errorHandler(error);
137
+ }
138
+ });
139
+
140
+ this.state = 'connected';
141
+ }
142
+
143
+ /**
144
+ * Stop the transport
145
+ */
146
+ stop(): void {
147
+ if (this.readline) {
148
+ this.readline.close();
149
+ this.readline = null;
150
+ }
151
+ this.state = 'closed';
152
+ }
153
+
154
+ /**
155
+ * Send a response
156
+ */
157
+ send(message: JsonRpcResponse | JsonRpcNotification): void {
158
+ if (this.state !== 'connected') {
159
+ throw new Error('Transport not connected');
160
+ }
161
+
162
+ const json = JSON.stringify(message);
163
+ this.output.write(json + '\n');
164
+ }
165
+
166
+ /**
167
+ * Handle incoming line
168
+ */
169
+ private async handleLine(line: string): Promise<void> {
170
+ if (!line.trim()) {
171
+ return;
172
+ }
173
+
174
+ try {
175
+ const message = JSON.parse(line) as JsonRpcRequest | JsonRpcNotification;
176
+
177
+ // Validate JSON-RPC message
178
+ if (!this.isValidJsonRpcMessage(message)) {
179
+ this.sendError(null, JsonRpcErrorCode.InvalidRequest, 'Invalid Request');
180
+ return;
181
+ }
182
+
183
+ if (this.messageHandler) {
184
+ const response = await this.messageHandler(message);
185
+ if (response) {
186
+ this.send(response);
187
+ }
188
+ }
189
+ } catch (error) {
190
+ // Parse error
191
+ this.sendError(null, JsonRpcErrorCode.ParseError, 'Parse error');
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Validate JSON-RPC message
197
+ */
198
+ private isValidJsonRpcMessage(
199
+ message: unknown
200
+ ): message is JsonRpcRequest | JsonRpcNotification {
201
+ if (typeof message !== 'object' || message === null) {
202
+ return false;
203
+ }
204
+
205
+ const msg = message as Record<string, unknown>;
206
+ if (msg['jsonrpc'] !== '2.0') {
207
+ return false;
208
+ }
209
+
210
+ if (typeof msg['method'] !== 'string') {
211
+ return false;
212
+ }
213
+
214
+ return true;
215
+ }
216
+
217
+ /**
218
+ * Send error response
219
+ */
220
+ private sendError(
221
+ id: string | number | null,
222
+ code: number,
223
+ message: string,
224
+ data?: unknown
225
+ ): void {
226
+ const response: JsonRpcResponse = {
227
+ jsonrpc: '2.0',
228
+ id: id ?? 0,
229
+ error: {
230
+ code,
231
+ message,
232
+ data,
233
+ },
234
+ };
235
+
236
+ this.send(response);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Create a success response
242
+ */
243
+ export function createSuccessResponse(
244
+ id: string | number,
245
+ result: unknown
246
+ ): JsonRpcResponse {
247
+ return {
248
+ jsonrpc: '2.0',
249
+ id,
250
+ result,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Create an error response
256
+ */
257
+ export function createErrorResponse(
258
+ id: string | number,
259
+ code: number,
260
+ message: string,
261
+ data?: unknown
262
+ ): JsonRpcResponse {
263
+ return {
264
+ jsonrpc: '2.0',
265
+ id,
266
+ error: {
267
+ code,
268
+ message,
269
+ data,
270
+ },
271
+ };
272
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * MCP Server テスト
3
+ *
4
+ * @task TSK-060
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import { KatashiroMCPServer } from '../../src/server/mcp-server.js';
9
+ import { isOk } from '@nahisaho/katashiro-core';
10
+
11
+ describe('KatashiroMCPServer', () => {
12
+ let server: KatashiroMCPServer;
13
+
14
+ beforeEach(() => {
15
+ server = new KatashiroMCPServer();
16
+ });
17
+
18
+ describe('initialization', () => {
19
+ it('should initialize with default config', () => {
20
+ expect(server).toBeDefined();
21
+ expect(server.getName()).toBe('katashiro');
22
+ });
23
+
24
+ it('should have server info', () => {
25
+ const info = server.getServerInfo();
26
+ expect(info.name).toBe('katashiro');
27
+ expect(info.version).toBe('0.1.0');
28
+ });
29
+
30
+ it('should have capabilities', () => {
31
+ const caps = server.getCapabilities();
32
+ expect(caps.tools).toBeDefined();
33
+ expect(caps.resources).toBeDefined();
34
+ expect(caps.prompts).toBeDefined();
35
+ });
36
+
37
+ it('should have all tools registered', () => {
38
+ const tools = server.getTools();
39
+ expect(tools.length).toBeGreaterThan(0);
40
+
41
+ const toolNames = tools.map((t) => t.name);
42
+ expect(toolNames).toContain('web_search');
43
+ expect(toolNames).toContain('analyze_content');
44
+ expect(toolNames).toContain('generate_summary');
45
+ expect(toolNames).toContain('query_knowledge');
46
+ expect(toolNames).toContain('generate_report');
47
+ });
48
+
49
+ it('should have all prompts registered', () => {
50
+ const prompts = server.getPrompts();
51
+ expect(prompts.length).toBeGreaterThan(0);
52
+
53
+ const promptNames = prompts.map((p) => p.name);
54
+ expect(promptNames).toContain('research_topic');
55
+ expect(promptNames).toContain('analyze_document');
56
+ expect(promptNames).toContain('create_presentation');
57
+ });
58
+ });
59
+
60
+ describe('tool execution', () => {
61
+ it('should execute web_search tool', async () => {
62
+ const result = await server.executeTool('web_search', {
63
+ query: 'TypeScript best practices',
64
+ });
65
+
66
+ expect(isOk(result)).toBe(true);
67
+ if (isOk(result)) {
68
+ expect(result.value.content).toBeDefined();
69
+ expect(result.value.content[0].type).toBe('text');
70
+ }
71
+ });
72
+
73
+ it('should execute analyze_content tool', async () => {
74
+ const result = await server.executeTool('analyze_content', {
75
+ content: 'This is a sample text for analysis.',
76
+ type: 'text',
77
+ });
78
+
79
+ expect(isOk(result)).toBe(true);
80
+ if (isOk(result)) {
81
+ expect(result.value.content).toBeDefined();
82
+ }
83
+ });
84
+
85
+ it('should execute generate_summary tool', async () => {
86
+ const result = await server.executeTool('generate_summary', {
87
+ content: 'Long article about AI developments in 2025...',
88
+ style: 'brief',
89
+ });
90
+
91
+ expect(isOk(result)).toBe(true);
92
+ });
93
+
94
+ it('should execute query_knowledge tool', async () => {
95
+ const result = await server.executeTool('query_knowledge', {
96
+ query: 'AI research',
97
+ });
98
+
99
+ expect(isOk(result)).toBe(true);
100
+ });
101
+
102
+ it('should execute generate_report tool', async () => {
103
+ const result = await server.executeTool('generate_report', {
104
+ topic: 'Market Analysis',
105
+ format: 'markdown',
106
+ });
107
+
108
+ expect(isOk(result)).toBe(true);
109
+ });
110
+
111
+ it('should return error for unknown tool', async () => {
112
+ const result = await server.executeTool('unknown_tool', {});
113
+ expect(isOk(result)).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe('prompt execution', () => {
118
+ it('should execute research_topic prompt', async () => {
119
+ const result = await server.executePrompt('research_topic', {
120
+ topic: 'Machine Learning',
121
+ });
122
+
123
+ expect(isOk(result)).toBe(true);
124
+ if (isOk(result)) {
125
+ expect(result.value.messages).toBeDefined();
126
+ expect(result.value.messages.length).toBeGreaterThan(0);
127
+ }
128
+ });
129
+
130
+ it('should execute analyze_document prompt', async () => {
131
+ const result = await server.executePrompt('analyze_document', {
132
+ document: 'Sample document content',
133
+ });
134
+
135
+ expect(isOk(result)).toBe(true);
136
+ });
137
+
138
+ it('should return error for unknown prompt', async () => {
139
+ const result = await server.executePrompt('unknown_prompt', {});
140
+ expect(isOk(result)).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe('resource management', () => {
145
+ it('should list resources', async () => {
146
+ const result = await server.listResources();
147
+ expect(isOk(result)).toBe(true);
148
+ if (isOk(result)) {
149
+ expect(Array.isArray(result.value)).toBe(true);
150
+ expect(result.value.length).toBeGreaterThan(0);
151
+ }
152
+ });
153
+
154
+ it('should read resource', async () => {
155
+ const result = await server.readResource('katashiro://knowledge/graph');
156
+ expect(isOk(result)).toBe(true);
157
+ });
158
+
159
+ it('should return error for unknown resource', async () => {
160
+ const result = await server.readResource('katashiro://unknown/resource');
161
+ expect(isOk(result)).toBe(false);
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,193 @@
1
+ /**
2
+ * PromptRegistry テスト
3
+ *
4
+ * @task TSK-062
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import { PromptRegistry } from '../../src/prompts/prompt-registry.js';
9
+ import { isOk } from '@nahisaho/katashiro-core';
10
+
11
+ describe('PromptRegistry', () => {
12
+ let registry: PromptRegistry;
13
+
14
+ beforeEach(() => {
15
+ registry = new PromptRegistry();
16
+ });
17
+
18
+ describe('registration', () => {
19
+ it('should register a prompt', () => {
20
+ const result = registry.register({
21
+ name: 'test_prompt',
22
+ description: 'A test prompt',
23
+ template: 'Hello, {{name}}!',
24
+ });
25
+
26
+ expect(isOk(result)).toBe(true);
27
+ });
28
+
29
+ it('should register prompt with arguments', () => {
30
+ const result = registry.register({
31
+ name: 'greeting',
32
+ description: 'Greeting prompt',
33
+ template: 'Hello, {{name}}! Welcome to {{place}}.',
34
+ arguments: [
35
+ { name: 'name', description: 'User name', required: true },
36
+ { name: 'place', description: 'Location', required: false },
37
+ ],
38
+ });
39
+
40
+ expect(isOk(result)).toBe(true);
41
+ });
42
+
43
+ it('should reject duplicate registration', () => {
44
+ registry.register({
45
+ name: 'test',
46
+ description: 'First',
47
+ template: 'First template',
48
+ });
49
+
50
+ const result = registry.register({
51
+ name: 'test',
52
+ description: 'Second',
53
+ template: 'Second template',
54
+ });
55
+
56
+ expect(isOk(result)).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe('lookup', () => {
61
+ it('should get registered prompt', () => {
62
+ registry.register({
63
+ name: 'my_prompt',
64
+ description: 'My prompt',
65
+ template: 'Template',
66
+ });
67
+
68
+ const result = registry.get('my_prompt');
69
+ expect(isOk(result)).toBe(true);
70
+ if (isOk(result)) {
71
+ expect(result.value?.name).toBe('my_prompt');
72
+ }
73
+ });
74
+
75
+ it('should return null for unknown prompt', () => {
76
+ const result = registry.get('unknown');
77
+ expect(isOk(result)).toBe(true);
78
+ if (isOk(result)) {
79
+ expect(result.value).toBeNull();
80
+ }
81
+ });
82
+
83
+ it('should list all prompts', () => {
84
+ registry.register({
85
+ name: 'prompt1',
86
+ description: 'Prompt 1',
87
+ template: 'T1',
88
+ });
89
+ registry.register({
90
+ name: 'prompt2',
91
+ description: 'Prompt 2',
92
+ template: 'T2',
93
+ });
94
+
95
+ const result = registry.list();
96
+ expect(isOk(result)).toBe(true);
97
+ if (isOk(result)) {
98
+ expect(result.value).toHaveLength(2);
99
+ }
100
+ });
101
+ });
102
+
103
+ describe('rendering', () => {
104
+ it('should render template with arguments', () => {
105
+ registry.register({
106
+ name: 'greeting',
107
+ description: 'Greeting',
108
+ template: 'Hello, {{name}}!',
109
+ });
110
+
111
+ const result = registry.render('greeting', { name: 'World' });
112
+ expect(isOk(result)).toBe(true);
113
+ if (isOk(result)) {
114
+ expect(result.value).toBe('Hello, World!');
115
+ }
116
+ });
117
+
118
+ it('should render multiple placeholders', () => {
119
+ registry.register({
120
+ name: 'intro',
121
+ description: 'Introduction',
122
+ template: 'I am {{name}}, age {{age}}, from {{city}}.',
123
+ });
124
+
125
+ const result = registry.render('intro', {
126
+ name: 'Alice',
127
+ age: '30',
128
+ city: 'Tokyo',
129
+ });
130
+
131
+ expect(isOk(result)).toBe(true);
132
+ if (isOk(result)) {
133
+ expect(result.value).toBe('I am Alice, age 30, from Tokyo.');
134
+ }
135
+ });
136
+
137
+ it('should handle missing arguments', () => {
138
+ registry.register({
139
+ name: 'test',
140
+ description: 'Test',
141
+ template: 'Hello, {{name}}!',
142
+ });
143
+
144
+ const result = registry.render('test', {});
145
+ expect(isOk(result)).toBe(true);
146
+ if (isOk(result)) {
147
+ expect(result.value).toContain('{{name}}');
148
+ }
149
+ });
150
+
151
+ it('should return error for unknown prompt', () => {
152
+ const result = registry.render('unknown', {});
153
+ expect(isOk(result)).toBe(false);
154
+ });
155
+ });
156
+
157
+ describe('validation', () => {
158
+ it('should validate required arguments', () => {
159
+ registry.register({
160
+ name: 'required_test',
161
+ description: 'Test required',
162
+ template: '{{required_arg}}',
163
+ arguments: [
164
+ { name: 'required_arg', description: 'Required', required: true },
165
+ ],
166
+ });
167
+
168
+ const result = registry.validate('required_test', {});
169
+ expect(isOk(result)).toBe(true);
170
+ if (isOk(result)) {
171
+ expect(result.value.valid).toBe(false);
172
+ expect(result.value.missing).toContain('required_arg');
173
+ }
174
+ });
175
+
176
+ it('should pass validation with all required args', () => {
177
+ registry.register({
178
+ name: 'valid_test',
179
+ description: 'Test',
180
+ template: '{{arg}}',
181
+ arguments: [
182
+ { name: 'arg', description: 'Arg', required: true },
183
+ ],
184
+ });
185
+
186
+ const result = registry.validate('valid_test', { arg: 'value' });
187
+ expect(isOk(result)).toBe(true);
188
+ if (isOk(result)) {
189
+ expect(result.value.valid).toBe(true);
190
+ }
191
+ });
192
+ });
193
+ });