mcp-rubber-duck 1.7.0 → 1.8.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/CHANGELOG.md +7 -0
- package/README.md +116 -1
- package/dist/prompts/architecture.d.ts +6 -0
- package/dist/prompts/architecture.d.ts.map +1 -0
- package/dist/prompts/architecture.js +103 -0
- package/dist/prompts/architecture.js.map +1 -0
- package/dist/prompts/assumptions.d.ts +6 -0
- package/dist/prompts/assumptions.d.ts.map +1 -0
- package/dist/prompts/assumptions.js +72 -0
- package/dist/prompts/assumptions.js.map +1 -0
- package/dist/prompts/blindspots.d.ts +6 -0
- package/dist/prompts/blindspots.d.ts.map +1 -0
- package/dist/prompts/blindspots.js +71 -0
- package/dist/prompts/blindspots.js.map +1 -0
- package/dist/prompts/diverge-converge.d.ts +6 -0
- package/dist/prompts/diverge-converge.d.ts.map +1 -0
- package/dist/prompts/diverge-converge.js +85 -0
- package/dist/prompts/diverge-converge.js.map +1 -0
- package/dist/prompts/index.d.ts +22 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +57 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/perspectives.d.ts +7 -0
- package/dist/prompts/perspectives.d.ts.map +1 -0
- package/dist/prompts/perspectives.js +65 -0
- package/dist/prompts/perspectives.js.map +1 -0
- package/dist/prompts/red-team.d.ts +6 -0
- package/dist/prompts/red-team.d.ts.map +1 -0
- package/dist/prompts/red-team.js +83 -0
- package/dist/prompts/red-team.js.map +1 -0
- package/dist/prompts/reframe.d.ts +6 -0
- package/dist/prompts/reframe.d.ts.map +1 -0
- package/dist/prompts/reframe.js +71 -0
- package/dist/prompts/reframe.js.map +1 -0
- package/dist/prompts/tradeoffs.d.ts +6 -0
- package/dist/prompts/tradeoffs.d.ts.map +1 -0
- package/dist/prompts/tradeoffs.js +87 -0
- package/dist/prompts/tradeoffs.js.map +1 -0
- package/dist/prompts/types.d.ts +14 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/prompts/types.js +2 -0
- package/dist/prompts/types.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +20 -1
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/prompts/architecture.ts +111 -0
- package/src/prompts/assumptions.ts +80 -0
- package/src/prompts/blindspots.ts +79 -0
- package/src/prompts/diverge-converge.ts +92 -0
- package/src/prompts/index.ts +63 -0
- package/src/prompts/perspectives.ts +73 -0
- package/src/prompts/red-team.ts +91 -0
- package/src/prompts/reframe.ts +78 -0
- package/src/prompts/tradeoffs.ts +95 -0
- package/src/prompts/types.ts +14 -0
- package/src/server.ts +23 -0
- package/tests/prompts.test.ts +314 -0
package/src/server.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
3
3
|
import {
|
|
4
4
|
CallToolRequestSchema,
|
|
5
5
|
ListToolsRequestSchema,
|
|
6
|
+
ListPromptsRequestSchema,
|
|
7
|
+
GetPromptRequestSchema,
|
|
6
8
|
Tool,
|
|
7
9
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
8
10
|
|
|
@@ -42,6 +44,9 @@ import { mcpStatusTool } from './tools/mcp-status.js';
|
|
|
42
44
|
// Import usage stats tool
|
|
43
45
|
import { getUsageStatsTool } from './tools/get-usage-stats.js';
|
|
44
46
|
|
|
47
|
+
// Import prompts
|
|
48
|
+
import { getPrompts, getPrompt } from './prompts/index.js';
|
|
49
|
+
|
|
45
50
|
export class RubberDuckServer {
|
|
46
51
|
private server: Server;
|
|
47
52
|
private configManager: ConfigManager;
|
|
@@ -68,6 +73,7 @@ export class RubberDuckServer {
|
|
|
68
73
|
{
|
|
69
74
|
capabilities: {
|
|
70
75
|
tools: {},
|
|
76
|
+
prompts: {},
|
|
71
77
|
},
|
|
72
78
|
}
|
|
73
79
|
);
|
|
@@ -142,6 +148,23 @@ export class RubberDuckServer {
|
|
|
142
148
|
return { tools: this.getTools() };
|
|
143
149
|
});
|
|
144
150
|
|
|
151
|
+
// List available prompts
|
|
152
|
+
this.server.setRequestHandler(ListPromptsRequestSchema, () => {
|
|
153
|
+
return { prompts: getPrompts() };
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Get specific prompt
|
|
157
|
+
this.server.setRequestHandler(GetPromptRequestSchema, (request) => {
|
|
158
|
+
const { name, arguments: args } = request.params;
|
|
159
|
+
try {
|
|
160
|
+
return getPrompt(name, args || {});
|
|
161
|
+
} catch (error: unknown) {
|
|
162
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
163
|
+
logger.error(`Prompt error for ${name}:`, errorMessage);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
145
168
|
// Handle tool calls
|
|
146
169
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
147
170
|
const { name, arguments: args } = request.params;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { getPrompts, getPrompt, PROMPTS } from '../src/prompts/index.js';
|
|
3
|
+
|
|
4
|
+
describe('Prompts', () => {
|
|
5
|
+
describe('getPrompts', () => {
|
|
6
|
+
it('should return all 8 prompts', () => {
|
|
7
|
+
const prompts = getPrompts();
|
|
8
|
+
expect(prompts).toHaveLength(8);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return prompts with required fields', () => {
|
|
12
|
+
const prompts = getPrompts();
|
|
13
|
+
for (const prompt of prompts) {
|
|
14
|
+
expect(prompt).toHaveProperty('name');
|
|
15
|
+
expect(prompt).toHaveProperty('description');
|
|
16
|
+
expect(typeof prompt.name).toBe('string');
|
|
17
|
+
expect(typeof prompt.description).toBe('string');
|
|
18
|
+
expect(prompt.name.length).toBeGreaterThan(0);
|
|
19
|
+
expect(prompt.description.length).toBeGreaterThan(0);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return prompts with arguments array', () => {
|
|
24
|
+
const prompts = getPrompts();
|
|
25
|
+
for (const prompt of prompts) {
|
|
26
|
+
expect(Array.isArray(prompt.arguments)).toBe(true);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should not expose buildMessages function', () => {
|
|
31
|
+
const prompts = getPrompts();
|
|
32
|
+
for (const prompt of prompts) {
|
|
33
|
+
expect(prompt).not.toHaveProperty('buildMessages');
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should have unique prompt names', () => {
|
|
38
|
+
const prompts = getPrompts();
|
|
39
|
+
const names = prompts.map((p) => p.name);
|
|
40
|
+
const uniqueNames = new Set(names);
|
|
41
|
+
expect(uniqueNames.size).toBe(names.length);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('getPrompt', () => {
|
|
46
|
+
it('should throw for unknown prompt', () => {
|
|
47
|
+
expect(() => getPrompt('nonexistent', {})).toThrow('Unknown prompt: nonexistent');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should return description and messages', () => {
|
|
51
|
+
const result = getPrompt('reframe', { problem: 'Test problem' });
|
|
52
|
+
expect(result).toHaveProperty('description');
|
|
53
|
+
expect(result).toHaveProperty('messages');
|
|
54
|
+
expect(Array.isArray(result.messages)).toBe(true);
|
|
55
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw for missing required arguments', () => {
|
|
59
|
+
expect(() => getPrompt('perspectives', {})).toThrow();
|
|
60
|
+
expect(() => getPrompt('perspectives', { problem: 'test' })).toThrow(); // missing perspectives
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should wrap errors with prompt context', () => {
|
|
64
|
+
try {
|
|
65
|
+
getPrompt('perspectives', {});
|
|
66
|
+
fail('Should have thrown');
|
|
67
|
+
} catch (e) {
|
|
68
|
+
expect((e as Error).message).toMatch(/^Failed to build prompt "perspectives":/);
|
|
69
|
+
expect((e as Error).message).toContain('problem argument is required');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle empty string arguments as missing', () => {
|
|
74
|
+
expect(() => getPrompt('perspectives', { problem: '', perspectives: 'test' })).toThrow(
|
|
75
|
+
'problem argument is required'
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle whitespace-only arguments as valid', () => {
|
|
80
|
+
// Whitespace is truthy in JS, so it passes validation (this is intentional)
|
|
81
|
+
const result = getPrompt('perspectives', { problem: ' ', perspectives: 'test' });
|
|
82
|
+
expect(result.messages).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('perspectives prompt', () => {
|
|
87
|
+
it('should require problem and perspectives arguments', () => {
|
|
88
|
+
expect(() => getPrompt('perspectives', {})).toThrow('problem argument is required');
|
|
89
|
+
expect(() => getPrompt('perspectives', { problem: 'test' })).toThrow(
|
|
90
|
+
'perspectives argument is required'
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should build valid messages with required args', () => {
|
|
95
|
+
const result = getPrompt('perspectives', {
|
|
96
|
+
problem: 'Test problem',
|
|
97
|
+
perspectives: 'security, performance',
|
|
98
|
+
});
|
|
99
|
+
expect(result.messages).toHaveLength(1);
|
|
100
|
+
expect(result.messages[0].role).toBe('user');
|
|
101
|
+
expect(result.messages[0].content).toHaveProperty('type', 'text');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should include optional context in message', () => {
|
|
105
|
+
const result = getPrompt('perspectives', {
|
|
106
|
+
problem: 'Test problem',
|
|
107
|
+
perspectives: 'security',
|
|
108
|
+
context: 'Additional context',
|
|
109
|
+
});
|
|
110
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
111
|
+
expect(text).toContain('Additional context');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('assumptions prompt', () => {
|
|
116
|
+
it('should require plan argument', () => {
|
|
117
|
+
expect(() => getPrompt('assumptions', {})).toThrow('plan argument is required');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should build valid messages', () => {
|
|
121
|
+
const result = getPrompt('assumptions', { plan: 'Test plan' });
|
|
122
|
+
expect(result.messages).toHaveLength(1);
|
|
123
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
124
|
+
expect(text).toContain('Test plan');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should include optional constraints and concerns', () => {
|
|
128
|
+
const result = getPrompt('assumptions', {
|
|
129
|
+
plan: 'Test plan',
|
|
130
|
+
constraints: 'Must be fast',
|
|
131
|
+
concerns: 'Worried about scale',
|
|
132
|
+
});
|
|
133
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
134
|
+
expect(text).toContain('Must be fast');
|
|
135
|
+
expect(text).toContain('Worried about scale');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('blindspots prompt', () => {
|
|
140
|
+
it('should require proposal argument', () => {
|
|
141
|
+
expect(() => getPrompt('blindspots', {})).toThrow('proposal argument is required');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should build valid messages', () => {
|
|
145
|
+
const result = getPrompt('blindspots', { proposal: 'Test proposal' });
|
|
146
|
+
expect(result.messages).toHaveLength(1);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('tradeoffs prompt', () => {
|
|
151
|
+
it('should require options and criteria arguments', () => {
|
|
152
|
+
expect(() => getPrompt('tradeoffs', {})).toThrow('options argument is required');
|
|
153
|
+
expect(() => getPrompt('tradeoffs', { options: 'A, B' })).toThrow(
|
|
154
|
+
'criteria argument is required'
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should build valid messages with required args', () => {
|
|
159
|
+
const result = getPrompt('tradeoffs', {
|
|
160
|
+
options: 'Option A, Option B',
|
|
161
|
+
criteria: 'cost, speed',
|
|
162
|
+
});
|
|
163
|
+
expect(result.messages).toHaveLength(1);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('red_team prompt', () => {
|
|
168
|
+
it('should require target argument', () => {
|
|
169
|
+
expect(() => getPrompt('red_team', {})).toThrow('target argument is required');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should build valid messages', () => {
|
|
173
|
+
const result = getPrompt('red_team', { target: 'Test system' });
|
|
174
|
+
expect(result.messages).toHaveLength(1);
|
|
175
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
176
|
+
expect(text).toContain('Test system');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('reframe prompt', () => {
|
|
181
|
+
it('should require problem argument', () => {
|
|
182
|
+
expect(() => getPrompt('reframe', {})).toThrow('problem argument is required');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should include three reframing types', () => {
|
|
186
|
+
const result = getPrompt('reframe', { problem: 'Test problem' });
|
|
187
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
188
|
+
expect(text).toContain('HIGHER ABSTRACTION');
|
|
189
|
+
expect(text).toContain('INVERSION');
|
|
190
|
+
expect(text).toContain('SIMPLIFICATION');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('architecture prompt', () => {
|
|
195
|
+
it('should require design, workloads, and priorities arguments', () => {
|
|
196
|
+
expect(() => getPrompt('architecture', {})).toThrow('design argument is required');
|
|
197
|
+
expect(() => getPrompt('architecture', { design: 'test' })).toThrow(
|
|
198
|
+
'workloads argument is required'
|
|
199
|
+
);
|
|
200
|
+
expect(() => getPrompt('architecture', { design: 'test', workloads: 'test' })).toThrow(
|
|
201
|
+
'priorities argument is required'
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should build valid messages with all required args', () => {
|
|
206
|
+
const result = getPrompt('architecture', {
|
|
207
|
+
design: 'Microservices',
|
|
208
|
+
workloads: '1000 req/s',
|
|
209
|
+
priorities: 'latency, cost',
|
|
210
|
+
});
|
|
211
|
+
expect(result.messages).toHaveLength(1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should include cross-cutting concerns', () => {
|
|
215
|
+
const result = getPrompt('architecture', {
|
|
216
|
+
design: 'test',
|
|
217
|
+
workloads: 'test',
|
|
218
|
+
priorities: 'test',
|
|
219
|
+
});
|
|
220
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
221
|
+
expect(text).toContain('Scalability');
|
|
222
|
+
expect(text).toContain('Reliability');
|
|
223
|
+
expect(text).toContain('Operational Complexity');
|
|
224
|
+
expect(text).toContain('Developer Experience');
|
|
225
|
+
expect(text).toContain('Cost Efficiency');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('diverge_converge prompt', () => {
|
|
230
|
+
it('should require challenge argument', () => {
|
|
231
|
+
expect(() => getPrompt('diverge_converge', {})).toThrow('challenge argument is required');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should include diverge and converge phases', () => {
|
|
235
|
+
const result = getPrompt('diverge_converge', { challenge: 'Test challenge' });
|
|
236
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
237
|
+
expect(text).toContain('PHASE 1: DIVERGE');
|
|
238
|
+
expect(text).toContain('PHASE 2: CONVERGE');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should use default width and criteria when not provided', () => {
|
|
242
|
+
const result = getPrompt('diverge_converge', { challenge: 'Test challenge' });
|
|
243
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
244
|
+
expect(text).toContain('balanced');
|
|
245
|
+
expect(text).toContain('feasibility, impact, and effort required');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('PROMPTS registry', () => {
|
|
250
|
+
it('should have all expected prompts', () => {
|
|
251
|
+
const expectedNames = [
|
|
252
|
+
'perspectives',
|
|
253
|
+
'assumptions',
|
|
254
|
+
'blindspots',
|
|
255
|
+
'tradeoffs',
|
|
256
|
+
'red_team',
|
|
257
|
+
'reframe',
|
|
258
|
+
'architecture',
|
|
259
|
+
'diverge_converge',
|
|
260
|
+
];
|
|
261
|
+
for (const name of expectedNames) {
|
|
262
|
+
expect(PROMPTS).toHaveProperty(name);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should have buildMessages function for all prompts', () => {
|
|
267
|
+
for (const [name, prompt] of Object.entries(PROMPTS)) {
|
|
268
|
+
expect(typeof prompt.buildMessages).toBe('function');
|
|
269
|
+
expect(prompt.name).toBe(name);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('MCP spec compliance', () => {
|
|
275
|
+
it('should return messages with valid role (user or assistant)', () => {
|
|
276
|
+
const result = getPrompt('reframe', { problem: 'test' });
|
|
277
|
+
for (const message of result.messages) {
|
|
278
|
+
expect(['user', 'assistant']).toContain(message.role);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should return messages with valid content type', () => {
|
|
283
|
+
const result = getPrompt('reframe', { problem: 'test' });
|
|
284
|
+
for (const message of result.messages) {
|
|
285
|
+
const content = message.content as { type: string; text?: string };
|
|
286
|
+
expect(['text', 'image', 'resource']).toContain(content.type);
|
|
287
|
+
if (content.type === 'text') {
|
|
288
|
+
expect(typeof content.text).toBe('string');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should preserve user input in generated messages', () => {
|
|
294
|
+
const userInput = 'Special chars: <>&"\' and unicode: 日本語';
|
|
295
|
+
const result = getPrompt('reframe', { problem: userInput });
|
|
296
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
297
|
+
expect(text).toContain(userInput);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should handle very long inputs without truncation', () => {
|
|
301
|
+
const longInput = 'x'.repeat(50000);
|
|
302
|
+
const result = getPrompt('reframe', { problem: longInput });
|
|
303
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
304
|
+
expect(text).toContain(longInput);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should handle inputs with newlines and special whitespace', () => {
|
|
308
|
+
const multilineInput = 'Line 1\nLine 2\n\tTabbed line\r\nWindows line';
|
|
309
|
+
const result = getPrompt('reframe', { problem: multilineInput });
|
|
310
|
+
const text = (result.messages[0].content as { type: string; text: string }).text;
|
|
311
|
+
expect(text).toContain(multilineInput);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|