@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,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
|
+
});
|