@echoes-io/mcp-server 1.7.0 → 1.8.1
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/lib/prompts/handlers.d.ts +21 -0
- package/lib/prompts/handlers.js +107 -0
- package/lib/prompts/index.d.ts +2 -0
- package/lib/prompts/index.js +2 -0
- package/lib/prompts/substitution.d.ts +2 -0
- package/lib/prompts/substitution.js +45 -0
- package/lib/prompts/validation.d.ts +8 -0
- package/lib/prompts/validation.js +20 -0
- package/lib/server.js +47 -12
- package/package.json +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Tracker } from '@echoes-io/tracker';
|
|
2
|
+
export declare function listPrompts(): {
|
|
3
|
+
prompts: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
arguments: {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
required: boolean;
|
|
10
|
+
}[];
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
13
|
+
export declare function getPrompt(name: string, args: Record<string, string>, timeline: string, tracker: Tracker): Promise<{
|
|
14
|
+
messages: {
|
|
15
|
+
role: "user";
|
|
16
|
+
content: {
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
};
|
|
20
|
+
}[];
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { substitutePlaceholders } from './substitution.js';
|
|
5
|
+
import { validateGitHubRepo } from './validation.js';
|
|
6
|
+
const PROMPTS = [
|
|
7
|
+
{
|
|
8
|
+
name: 'new-chapter',
|
|
9
|
+
description: 'Create a new chapter for a timeline arc',
|
|
10
|
+
arguments: [
|
|
11
|
+
{ name: 'arc', description: 'Arc name (e.g., "work", "anima")', required: true },
|
|
12
|
+
{ name: 'chapter', description: 'Chapter number (e.g., "1", "12")', required: true },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'revise-chapter',
|
|
17
|
+
description: 'Revise an existing chapter with specific improvements',
|
|
18
|
+
arguments: [
|
|
19
|
+
{ name: 'arc', description: 'Arc name', required: true },
|
|
20
|
+
{ name: 'chapter', description: 'Chapter number', required: true },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'expand-chapter',
|
|
25
|
+
description: 'Expand a chapter to reach target word count',
|
|
26
|
+
arguments: [
|
|
27
|
+
{ name: 'arc', description: 'Arc name', required: true },
|
|
28
|
+
{ name: 'chapter', description: 'Chapter number', required: true },
|
|
29
|
+
{ name: 'target', description: 'Target word count (e.g., "4000")', required: true },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'new-character',
|
|
34
|
+
description: 'Create a new character sheet',
|
|
35
|
+
arguments: [{ name: 'name', description: 'Character name', required: true }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'new-episode',
|
|
39
|
+
description: 'Create a new episode outline',
|
|
40
|
+
arguments: [
|
|
41
|
+
{ name: 'arc', description: 'Arc name', required: true },
|
|
42
|
+
{ name: 'episode', description: 'Episode number', required: true },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'new-arc',
|
|
47
|
+
description: 'Create a new story arc',
|
|
48
|
+
arguments: [{ name: 'name', description: 'Arc name (lowercase, no spaces)', required: true }],
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
export function listPrompts() {
|
|
52
|
+
return { prompts: PROMPTS };
|
|
53
|
+
}
|
|
54
|
+
export async function getPrompt(name, args, timeline, tracker) {
|
|
55
|
+
try {
|
|
56
|
+
// Validate .github repo exists
|
|
57
|
+
const { exists: githubExists, path: githubPath } = validateGitHubRepo();
|
|
58
|
+
if (!githubExists) {
|
|
59
|
+
throw new Error('.github repository not found.\n' +
|
|
60
|
+
'Clone it as sibling: git clone https://github.com/echoes-io/.github ../.github');
|
|
61
|
+
}
|
|
62
|
+
// Read base template (required)
|
|
63
|
+
const basePath = join(githubPath, `${name}.md`);
|
|
64
|
+
if (!existsSync(basePath)) {
|
|
65
|
+
throw new Error(`Prompt template not found: ${name}.md\n` +
|
|
66
|
+
`Expected location: ../.github/.kiro/prompts/${name}.md`);
|
|
67
|
+
}
|
|
68
|
+
const basePrompt = await readFile(basePath, 'utf-8');
|
|
69
|
+
// Check for timeline override (optional)
|
|
70
|
+
const timelinePromptsPath = resolve(process.cwd(), '.kiro/prompts');
|
|
71
|
+
const overridePath = join(timelinePromptsPath, `${name}.md`);
|
|
72
|
+
let overridePrompt = '';
|
|
73
|
+
if (existsSync(overridePath)) {
|
|
74
|
+
overridePrompt = await readFile(overridePath, 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
// Concatenate (base first, then override)
|
|
77
|
+
const combinedPrompt = overridePrompt
|
|
78
|
+
? `${basePrompt}\n\n---\n\n${overridePrompt}`
|
|
79
|
+
: basePrompt;
|
|
80
|
+
// Substitute placeholders
|
|
81
|
+
const finalPrompt = await substitutePlaceholders(name, combinedPrompt, args, timeline, tracker);
|
|
82
|
+
return {
|
|
83
|
+
messages: [
|
|
84
|
+
{
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: {
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: finalPrompt,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
messages: [
|
|
97
|
+
{
|
|
98
|
+
role: 'user',
|
|
99
|
+
content: {
|
|
100
|
+
type: 'text',
|
|
101
|
+
text: `❌ Error loading prompt "${name}":\n\n${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getAvailableArcs, validateArcExists, validateArcNotExists } from './validation.js';
|
|
2
|
+
export async function substitutePlaceholders(promptName, template, args, timeline, tracker) {
|
|
3
|
+
const replacements = {
|
|
4
|
+
TIMELINE: timeline,
|
|
5
|
+
...args,
|
|
6
|
+
};
|
|
7
|
+
// Prompt-specific validations
|
|
8
|
+
if (['new-chapter', 'revise-chapter', 'expand-chapter'].includes(promptName)) {
|
|
9
|
+
const { arc, chapter } = args;
|
|
10
|
+
if (!arc) {
|
|
11
|
+
throw new Error('Missing required argument: arc');
|
|
12
|
+
}
|
|
13
|
+
if (!chapter) {
|
|
14
|
+
throw new Error('Missing required argument: chapter');
|
|
15
|
+
}
|
|
16
|
+
// Validate arc exists
|
|
17
|
+
const arcExists = await validateArcExists(arc, tracker, timeline);
|
|
18
|
+
if (!arcExists) {
|
|
19
|
+
const available = await getAvailableArcs(tracker, timeline);
|
|
20
|
+
throw new Error(`Arc "${arc}" not found in tracker.\nAvailable arcs: ${available.join(', ') || 'none'}`);
|
|
21
|
+
}
|
|
22
|
+
// Validate chapter is a number
|
|
23
|
+
if (!/^\d+$/.test(chapter)) {
|
|
24
|
+
throw new Error(`Chapter must be a number, got: "${chapter}"`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (promptName === 'new-arc') {
|
|
28
|
+
const { name } = args;
|
|
29
|
+
if (!name) {
|
|
30
|
+
throw new Error('Missing required argument: name');
|
|
31
|
+
}
|
|
32
|
+
// Validate arc doesn't exist
|
|
33
|
+
const arcNotExists = await validateArcNotExists(name, tracker, timeline);
|
|
34
|
+
if (!arcNotExists) {
|
|
35
|
+
throw new Error(`Arc "${name}" already exists in tracker.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Replace all placeholders
|
|
39
|
+
let result = template;
|
|
40
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
41
|
+
const placeholder = `{${key.toUpperCase()}}`;
|
|
42
|
+
result = result.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), value);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Tracker } from '@echoes-io/tracker';
|
|
2
|
+
export declare function validateGitHubRepo(): {
|
|
3
|
+
exists: boolean;
|
|
4
|
+
path: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function validateArcExists(arc: string, tracker: Tracker, timeline: string): Promise<boolean>;
|
|
7
|
+
export declare function validateArcNotExists(arc: string, tracker: Tracker, timeline: string): Promise<boolean>;
|
|
8
|
+
export declare function getAvailableArcs(tracker: Tracker, timeline: string): Promise<string[]>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
export function validateGitHubRepo() {
|
|
4
|
+
const githubPath = resolve(process.cwd(), '../.github/.kiro/prompts');
|
|
5
|
+
return {
|
|
6
|
+
exists: existsSync(githubPath),
|
|
7
|
+
path: githubPath,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export async function validateArcExists(arc, tracker, timeline) {
|
|
11
|
+
const arcs = await tracker.getArcs(timeline);
|
|
12
|
+
return arcs.some((a) => a.name === arc);
|
|
13
|
+
}
|
|
14
|
+
export async function validateArcNotExists(arc, tracker, timeline) {
|
|
15
|
+
return !(await validateArcExists(arc, tracker, timeline));
|
|
16
|
+
}
|
|
17
|
+
export async function getAvailableArcs(tracker, timeline) {
|
|
18
|
+
const arcs = await tracker.getArcs(timeline);
|
|
19
|
+
return arcs.map((a) => a.name);
|
|
20
|
+
}
|
package/lib/server.js
CHANGED
|
@@ -6,7 +6,8 @@ import { Tracker } from '@echoes-io/tracker';
|
|
|
6
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import { toJsonSchemaCompat } from '@modelcontextprotocol/sdk/server/zod-json-schema-compat.js';
|
|
9
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { getPrompt, listPrompts, validateGitHubRepo } from './prompts/index.js';
|
|
10
11
|
import { bookGenerate, bookGenerateSchema, chapterDelete, chapterDeleteSchema, chapterInfo, chapterInfoSchema, chapterInsert, chapterInsertSchema, chapterRefresh, chapterRefreshSchema, episodeInfo, episodeInfoSchema, episodeUpdate, episodeUpdateSchema, ragCharacters, ragCharactersSchema, ragContext, ragContextSchema, ragIndex, ragIndexSchema, ragSearch, ragSearchSchema, stats, statsSchema, timelineSync, timelineSyncSchema, wordsCount, wordsCountSchema, } from './tools/index.js';
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
@@ -17,6 +18,7 @@ export function createServer(timelines) {
|
|
|
17
18
|
}, {
|
|
18
19
|
capabilities: {
|
|
19
20
|
tools: {},
|
|
21
|
+
prompts: {},
|
|
20
22
|
},
|
|
21
23
|
});
|
|
22
24
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -177,6 +179,23 @@ export function createServer(timelines) {
|
|
|
177
179
|
throw new Error(`Unknown tool: ${name}`);
|
|
178
180
|
}
|
|
179
181
|
});
|
|
182
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
183
|
+
return listPrompts();
|
|
184
|
+
});
|
|
185
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
186
|
+
const { name, arguments: args } = request.params;
|
|
187
|
+
// Get timeline from first available timeline (single-timeline mode)
|
|
188
|
+
// or require timeline in args for multi-timeline mode
|
|
189
|
+
const timelineNames = Array.from(timelines.keys());
|
|
190
|
+
if (timelineNames.length === 0) {
|
|
191
|
+
throw new Error('No timelines available');
|
|
192
|
+
}
|
|
193
|
+
// For single-timeline mode, use the only timeline
|
|
194
|
+
// For multi-timeline mode, this would need timeline in args
|
|
195
|
+
const timeline = timelineNames[0];
|
|
196
|
+
const { tracker } = timelines.get(timeline);
|
|
197
|
+
return await getPrompt(name, args || {}, timeline, tracker);
|
|
198
|
+
});
|
|
180
199
|
return server;
|
|
181
200
|
}
|
|
182
201
|
export async function runServer() {
|
|
@@ -185,7 +204,12 @@ export async function runServer() {
|
|
|
185
204
|
const cwd = process.cwd();
|
|
186
205
|
const cwdName = basename(cwd);
|
|
187
206
|
const timelines = new Map();
|
|
188
|
-
|
|
207
|
+
const isTest = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true';
|
|
208
|
+
const log = (...args) => {
|
|
209
|
+
if (!isTest)
|
|
210
|
+
console.error(...args);
|
|
211
|
+
};
|
|
212
|
+
log(`[DEBUG] Starting from: ${cwd}`);
|
|
189
213
|
if (process.env.NODE_ENV === 'test') {
|
|
190
214
|
// Test mode: in-memory databases
|
|
191
215
|
const tracker = new Tracker(':memory:');
|
|
@@ -195,7 +219,7 @@ export async function runServer() {
|
|
|
195
219
|
dbPath: ':memory:',
|
|
196
220
|
});
|
|
197
221
|
timelines.set('test', { tracker, rag, contentPath: './test-content' });
|
|
198
|
-
|
|
222
|
+
log('[DEBUG] Mode: test (in-memory)');
|
|
199
223
|
}
|
|
200
224
|
else if (cwdName.startsWith('timeline-')) {
|
|
201
225
|
// Single timeline mode: running from timeline directory
|
|
@@ -215,10 +239,10 @@ export async function runServer() {
|
|
|
215
239
|
geminiApiKey: process.env.ECHOES_GEMINI_API_KEY,
|
|
216
240
|
});
|
|
217
241
|
timelines.set(timelineName, { tracker, rag, contentPath });
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
242
|
+
log(`[DEBUG] Mode: single-timeline "${timelineName}"`);
|
|
243
|
+
log(`[DEBUG] Content: ${contentPath}`);
|
|
244
|
+
log(`[DEBUG] Tracker: ${trackerPath}`);
|
|
245
|
+
log(`[DEBUG] RAG: ${ragPath}`);
|
|
222
246
|
}
|
|
223
247
|
else if (cwdName === 'mcp-server') {
|
|
224
248
|
// Test mode from mcp-server directory: in-memory
|
|
@@ -229,20 +253,20 @@ export async function runServer() {
|
|
|
229
253
|
dbPath: ':memory:',
|
|
230
254
|
});
|
|
231
255
|
timelines.set('test', { tracker, rag, contentPath: './test-content' });
|
|
232
|
-
|
|
256
|
+
log('[DEBUG] Mode: test from mcp-server (in-memory)');
|
|
233
257
|
}
|
|
234
258
|
else {
|
|
235
259
|
// Multi-timeline mode: discover from parent directory (backward compat for .github)
|
|
236
260
|
const parentDir = join(cwd, '..');
|
|
237
261
|
const entries = readdirSync(parentDir, { withFileTypes: true });
|
|
238
|
-
|
|
262
|
+
log(`[DEBUG] Mode: multi-timeline (scanning ${parentDir})`);
|
|
239
263
|
for (const entry of entries) {
|
|
240
264
|
if (entry.isDirectory() && entry.name.startsWith('timeline-')) {
|
|
241
265
|
const timelineName = entry.name.replace('timeline-', '');
|
|
242
266
|
const timelinePath = join(parentDir, entry.name);
|
|
243
267
|
const contentPath = join(timelinePath, 'content');
|
|
244
268
|
if (!existsSync(contentPath)) {
|
|
245
|
-
|
|
269
|
+
log(`[DEBUG] Skipping ${entry.name}: no content directory`);
|
|
246
270
|
continue;
|
|
247
271
|
}
|
|
248
272
|
const trackerPath = join(timelinePath, 'tracker.db');
|
|
@@ -256,7 +280,7 @@ export async function runServer() {
|
|
|
256
280
|
geminiApiKey: process.env.ECHOES_GEMINI_API_KEY,
|
|
257
281
|
});
|
|
258
282
|
timelines.set(timelineName, { tracker, rag, contentPath });
|
|
259
|
-
|
|
283
|
+
log(`[DEBUG] Timeline "${timelineName}": ${trackerPath}`);
|
|
260
284
|
}
|
|
261
285
|
}
|
|
262
286
|
if (timelines.size === 0) {
|
|
@@ -266,5 +290,16 @@ export async function runServer() {
|
|
|
266
290
|
const server = createServer(timelines);
|
|
267
291
|
const transport = new StdioServerTransport();
|
|
268
292
|
await server.connect(transport);
|
|
269
|
-
|
|
293
|
+
log(`[DEBUG] Server ready with ${timelines.size} timeline(s)`);
|
|
294
|
+
// Validate .github repo for prompts
|
|
295
|
+
const { exists: githubExists } = validateGitHubRepo();
|
|
296
|
+
if (!githubExists) {
|
|
297
|
+
log('⚠️ WARNING: .github repository not found');
|
|
298
|
+
log(' Expected location: ../.github/.kiro/prompts/');
|
|
299
|
+
log(' MCP prompts will not work until .github repo is cloned as sibling.');
|
|
300
|
+
log(' Clone: git clone https://github.com/echoes-io/.github ../.github');
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
log('✓ .github repository found');
|
|
304
|
+
}
|
|
270
305
|
}
|
package/package.json
CHANGED