@echoes-io/mcp-server 1.1.0 → 1.3.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/README.md CHANGED
@@ -38,7 +38,9 @@ Then configure:
38
38
  "echoes": {
39
39
  "command": "echoes-mcp-server",
40
40
  "env": {
41
- "ECHOES_TIMELINE": "your-timeline-name"
41
+ "ECHOES_TIMELINE": "your-timeline-name",
42
+ "ECHOES_RAG_PROVIDER": "e5-small",
43
+ "ECHOES_RAG_DB_PATH": "./rag_data.db"
42
44
  }
43
45
  }
44
46
  }
@@ -47,6 +49,11 @@ Then configure:
47
49
 
48
50
  **Important:** The `ECHOES_TIMELINE` environment variable must be set to specify which timeline to work with. All tools operate on this timeline.
49
51
 
52
+ **Optional RAG Configuration:**
53
+ - `ECHOES_RAG_PROVIDER`: Embedding provider (`e5-small`, `e5-large`, or `gemini`). Default: `e5-small`
54
+ - `ECHOES_GEMINI_API_KEY`: Required if using `gemini` provider
55
+ - `ECHOES_RAG_DB_PATH`: SQLite database path. Default: `./rag_data.db`
56
+
50
57
  ## Available Tools
51
58
 
52
59
  All tools operate on the timeline specified by the `ECHOES_TIMELINE` environment variable.
@@ -88,6 +95,26 @@ All tools operate on the timeline specified by the `ECHOES_TIMELINE` environment
88
95
  - `arc: "arc1", episode: 1`: Statistics for specific episode
89
96
  - `pov: "Alice"`: Statistics for specific POV across timeline
90
97
 
98
+ ### RAG (Semantic Search)
99
+ - **`rag-index`** - Index chapters into vector database for semantic search
100
+ - Input: optional: `arc`, `episode` (to index specific content)
101
+ - Output: Number of chapters indexed
102
+
103
+ - **`rag-search`** - Semantic search across timeline content
104
+ - Input: `query`, optional: `arc`, `pov`, `maxResults`
105
+ - Output: Relevant chapters with similarity scores and previews
106
+
107
+ - **`rag-context`** - Retrieve relevant context for AI interactions
108
+ - Input: `query`, optional: `arc`, `pov`, `maxChapters`
109
+ - Output: Full chapter content for AI context
110
+
111
+ ### Book Generation
112
+ - **`book-generate`** - Generate PDF book from timeline content using LaTeX
113
+ - Input: `contentPath`, `outputPath`, optional: `episodes`, `format`
114
+ - Output: PDF book with Victoria Regia template
115
+ - Formats: `a4` (default), `a5`
116
+ - Requirements: pandoc, LaTeX distribution (pdflatex/xelatex/lualatex)
117
+
91
118
  ## Development
92
119
 
93
120
  ### Scripts
@@ -124,12 +151,6 @@ npm run lint:fix
124
151
  - **Testing**: Comprehensive unit and integration tests
125
152
  - **Environment**: Uses `ECHOES_TIMELINE` env var for timeline context
126
153
 
127
- ## Roadmap
128
-
129
- ### Planned Features
130
- - **Book generation** - LaTeX/PDF compilation tools for creating publishable books from timeline content
131
- - **RAG system** - Retrieval-Augmented Generation integration for AI-assisted writing and content analysis
132
-
133
154
  ## License
134
155
 
135
156
  MIT
package/lib/server.d.ts CHANGED
@@ -1,6 +1,7 @@
1
+ import { RAGSystem } from '@echoes-io/rag';
1
2
  import { Tracker } from '@echoes-io/tracker';
2
3
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- export declare function createServer(tracker: Tracker): Server<{
4
+ export declare function createServer(tracker: Tracker, rag: RAGSystem): Server<{
4
5
  method: string;
5
6
  params?: {
6
7
  [x: string]: unknown;
package/lib/server.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { RAGSystem } from '@echoes-io/rag';
4
5
  import { Tracker } from '@echoes-io/tracker';
5
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
8
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8
9
  import { zodToJsonSchema } from 'zod-to-json-schema';
9
- import { chapterDelete, chapterDeleteSchema, chapterInfo, chapterInfoSchema, chapterInsert, chapterInsertSchema, chapterRefresh, chapterRefreshSchema, episodeInfo, episodeInfoSchema, episodeUpdate, episodeUpdateSchema, stats, statsSchema, timelineSync, timelineSyncSchema, wordsCount, wordsCountSchema, } from './tools/index.js';
10
+ import { bookGenerate, bookGenerateSchema, chapterDelete, chapterDeleteSchema, chapterInfo, chapterInfoSchema, chapterInsert, chapterInsertSchema, chapterRefresh, chapterRefreshSchema, episodeInfo, episodeInfoSchema, episodeUpdate, episodeUpdateSchema, ragContext, ragContextSchema, ragIndex, ragIndexSchema, ragSearch, ragSearchSchema, stats, statsSchema, timelineSync, timelineSyncSchema, wordsCount, wordsCountSchema, } from './tools/index.js';
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
12
- export function createServer(tracker) {
13
+ export function createServer(tracker, rag) {
13
14
  const server = new Server({
14
15
  name: pkg.name,
15
16
  version: pkg.version,
@@ -66,6 +67,26 @@ export function createServer(tracker) {
66
67
  description: 'Get statistics for timeline, arc, episode, or POV',
67
68
  inputSchema: zodToJsonSchema(statsSchema),
68
69
  },
70
+ {
71
+ name: 'rag-index',
72
+ description: 'Index chapters into RAG vector database for semantic search',
73
+ inputSchema: zodToJsonSchema(ragIndexSchema),
74
+ },
75
+ {
76
+ name: 'rag-search',
77
+ description: 'Semantic search across timeline content',
78
+ inputSchema: zodToJsonSchema(ragSearchSchema),
79
+ },
80
+ {
81
+ name: 'rag-context',
82
+ description: 'Retrieve relevant context for AI interactions',
83
+ inputSchema: zodToJsonSchema(ragContextSchema),
84
+ },
85
+ {
86
+ name: 'book-generate',
87
+ description: 'Generate PDF book from timeline content using LaTeX',
88
+ inputSchema: zodToJsonSchema(bookGenerateSchema),
89
+ },
69
90
  ],
70
91
  };
71
92
  });
@@ -90,6 +111,14 @@ export function createServer(tracker) {
90
111
  return await timelineSync(timelineSyncSchema.parse(args), tracker);
91
112
  case 'stats':
92
113
  return await stats(statsSchema.parse(args), tracker);
114
+ case 'rag-index':
115
+ return await ragIndex(ragIndexSchema.parse(args), tracker, rag);
116
+ case 'rag-search':
117
+ return await ragSearch(ragSearchSchema.parse(args), rag);
118
+ case 'rag-context':
119
+ return await ragContext(ragContextSchema.parse(args), rag);
120
+ case 'book-generate':
121
+ return await bookGenerate(bookGenerateSchema.parse(args));
93
122
  default:
94
123
  throw new Error(`Unknown tool: ${name}`);
95
124
  }
@@ -102,7 +131,17 @@ export async function runServer() {
102
131
  const tracker = new Tracker(dbPath);
103
132
  await tracker.init();
104
133
  console.error(`Tracker database initialized: ${dbPath}`);
105
- const server = createServer(tracker);
134
+ // Initialize RAG system
135
+ const ragDbPath = process.env.ECHOES_RAG_DB_PATH ||
136
+ (process.env.NODE_ENV === 'test' ? ':memory:' : './rag_data.db');
137
+ const provider = (process.env.ECHOES_RAG_PROVIDER || 'e5-small');
138
+ const rag = new RAGSystem({
139
+ provider,
140
+ dbPath: ragDbPath,
141
+ geminiApiKey: process.env.ECHOES_GEMINI_API_KEY,
142
+ });
143
+ console.error(`RAG system initialized: ${ragDbPath} (provider: ${provider})`);
144
+ const server = createServer(tracker, rag);
106
145
  const transport = new StdioServerTransport();
107
146
  await server.connect(transport);
108
147
  console.error('Echoes MCP Server running on stdio');
@@ -0,0 +1,23 @@
1
+ import { z } from 'zod';
2
+ export declare const bookGenerateSchema: z.ZodObject<{
3
+ contentPath: z.ZodString;
4
+ outputPath: z.ZodString;
5
+ episodes: z.ZodOptional<z.ZodString>;
6
+ format: z.ZodOptional<z.ZodEnum<["a4", "a5"]>>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ contentPath: string;
9
+ outputPath: string;
10
+ episodes?: string | undefined;
11
+ format?: "a4" | "a5" | undefined;
12
+ }, {
13
+ contentPath: string;
14
+ outputPath: string;
15
+ episodes?: string | undefined;
16
+ format?: "a4" | "a5" | undefined;
17
+ }>;
18
+ export declare function bookGenerate(args: z.infer<typeof bookGenerateSchema>): Promise<{
19
+ content: {
20
+ type: "text";
21
+ text: string;
22
+ }[];
23
+ }>;
@@ -0,0 +1,38 @@
1
+ import { generateBook } from '@echoes-io/books-generator';
2
+ import { z } from 'zod';
3
+ import { getTimeline } from '../utils.js';
4
+ export const bookGenerateSchema = z.object({
5
+ contentPath: z.string().describe('Path to timeline content folder'),
6
+ outputPath: z.string().describe('Output PDF file path'),
7
+ episodes: z.string().optional().describe('Comma-separated episode numbers (e.g., "1,2,3")'),
8
+ format: z.enum(['a4', 'a5']).optional().describe('Page format (default: a4)'),
9
+ });
10
+ export async function bookGenerate(args) {
11
+ try {
12
+ const timeline = getTimeline();
13
+ await generateBook({
14
+ contentPath: args.contentPath,
15
+ outputPath: args.outputPath,
16
+ timeline,
17
+ episodes: args.episodes,
18
+ format: args.format || 'a4',
19
+ });
20
+ return {
21
+ content: [
22
+ {
23
+ type: 'text',
24
+ text: JSON.stringify({
25
+ success: true,
26
+ timeline,
27
+ outputPath: args.outputPath,
28
+ episodes: args.episodes || 'all',
29
+ format: args.format || 'a4',
30
+ }, null, 2),
31
+ },
32
+ ],
33
+ };
34
+ }
35
+ catch (error) {
36
+ throw new Error(`Failed to generate book: ${error instanceof Error ? error.message : 'Unknown error'}`);
37
+ }
38
+ }
@@ -1,9 +1,13 @@
1
+ export { bookGenerate, bookGenerateSchema } from './book-generate.js';
1
2
  export { chapterDelete, chapterDeleteSchema } from './chapter-delete.js';
2
3
  export { chapterInfo, chapterInfoSchema } from './chapter-info.js';
3
4
  export { chapterInsert, chapterInsertSchema } from './chapter-insert.js';
4
5
  export { chapterRefresh, chapterRefreshSchema } from './chapter-refresh.js';
5
6
  export { episodeInfo, episodeInfoSchema } from './episode-info.js';
6
7
  export { episodeUpdate, episodeUpdateSchema } from './episode-update.js';
8
+ export { ragContext, ragContextSchema } from './rag-context.js';
9
+ export { ragIndex, ragIndexSchema } from './rag-index.js';
10
+ export { ragSearch, ragSearchSchema } from './rag-search.js';
7
11
  export { stats, statsSchema } from './stats.js';
8
12
  export { timelineSync, timelineSyncSchema } from './timeline-sync.js';
9
13
  export { wordsCount, wordsCountSchema } from './words-count.js';
@@ -1,9 +1,13 @@
1
+ export { bookGenerate, bookGenerateSchema } from './book-generate.js';
1
2
  export { chapterDelete, chapterDeleteSchema } from './chapter-delete.js';
2
3
  export { chapterInfo, chapterInfoSchema } from './chapter-info.js';
3
4
  export { chapterInsert, chapterInsertSchema } from './chapter-insert.js';
4
5
  export { chapterRefresh, chapterRefreshSchema } from './chapter-refresh.js';
5
6
  export { episodeInfo, episodeInfoSchema } from './episode-info.js';
6
7
  export { episodeUpdate, episodeUpdateSchema } from './episode-update.js';
8
+ export { ragContext, ragContextSchema } from './rag-context.js';
9
+ export { ragIndex, ragIndexSchema } from './rag-index.js';
10
+ export { ragSearch, ragSearchSchema } from './rag-search.js';
7
11
  export { stats, statsSchema } from './stats.js';
8
12
  export { timelineSync, timelineSyncSchema } from './timeline-sync.js';
9
13
  export { wordsCount, wordsCountSchema } from './words-count.js';
@@ -0,0 +1,24 @@
1
+ import type { RAGSystem } from '@echoes-io/rag';
2
+ import { z } from 'zod';
3
+ export declare const ragContextSchema: z.ZodObject<{
4
+ query: z.ZodString;
5
+ arc: z.ZodOptional<z.ZodString>;
6
+ pov: z.ZodOptional<z.ZodString>;
7
+ maxChapters: z.ZodOptional<z.ZodNumber>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ query: string;
10
+ arc?: string | undefined;
11
+ pov?: string | undefined;
12
+ maxChapters?: number | undefined;
13
+ }, {
14
+ query: string;
15
+ arc?: string | undefined;
16
+ pov?: string | undefined;
17
+ maxChapters?: number | undefined;
18
+ }>;
19
+ export declare function ragContext(args: z.infer<typeof ragContextSchema>, rag: RAGSystem): Promise<{
20
+ content: {
21
+ type: "text";
22
+ text: string;
23
+ }[];
24
+ }>;
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ import { getTimeline } from '../utils.js';
3
+ export const ragContextSchema = z.object({
4
+ query: z.string().describe('Context query'),
5
+ arc: z.string().optional().describe('Filter by arc name'),
6
+ pov: z.string().optional().describe('Filter by POV character'),
7
+ maxChapters: z.number().optional().describe('Maximum number of chapters (default: 5)'),
8
+ });
9
+ export async function ragContext(args, rag) {
10
+ try {
11
+ const timeline = getTimeline();
12
+ const results = await rag.getContext({
13
+ query: args.query,
14
+ timeline,
15
+ arc: args.arc,
16
+ pov: args.pov,
17
+ maxChapters: args.maxChapters,
18
+ });
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify({
24
+ query: args.query,
25
+ timeline,
26
+ filters: {
27
+ arc: args.arc || null,
28
+ pov: args.pov || null,
29
+ },
30
+ context: results.map((r) => ({
31
+ chapter: {
32
+ arc: r.metadata.arcName,
33
+ episode: r.metadata.episodeNumber,
34
+ chapter: r.metadata.number,
35
+ pov: r.metadata.pov,
36
+ title: r.metadata.title,
37
+ },
38
+ similarity: r.similarity,
39
+ content: r.content,
40
+ })),
41
+ }, null, 2),
42
+ },
43
+ ],
44
+ };
45
+ }
46
+ catch (error) {
47
+ throw new Error(`Failed to get context: ${error instanceof Error ? error.message : 'Unknown error'}`);
48
+ }
49
+ }
@@ -0,0 +1,19 @@
1
+ import type { RAGSystem } from '@echoes-io/rag';
2
+ import type { Tracker } from '@echoes-io/tracker';
3
+ import { z } from 'zod';
4
+ export declare const ragIndexSchema: z.ZodObject<{
5
+ arc: z.ZodOptional<z.ZodString>;
6
+ episode: z.ZodOptional<z.ZodNumber>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ arc?: string | undefined;
9
+ episode?: number | undefined;
10
+ }, {
11
+ arc?: string | undefined;
12
+ episode?: number | undefined;
13
+ }>;
14
+ export declare function ragIndex(args: z.infer<typeof ragIndexSchema>, tracker: Tracker, rag: RAGSystem): Promise<{
15
+ content: {
16
+ type: "text";
17
+ text: string;
18
+ }[];
19
+ }>;
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+ import { getTimeline } from '../utils.js';
3
+ export const ragIndexSchema = z.object({
4
+ arc: z.string().optional().describe('Index specific arc only'),
5
+ episode: z.number().optional().describe('Index specific episode only (requires arc)'),
6
+ });
7
+ export async function ragIndex(args, tracker, rag) {
8
+ try {
9
+ const timeline = getTimeline();
10
+ let chapters = [];
11
+ // Get chapters based on filters
12
+ if (args.arc && args.episode) {
13
+ chapters = await tracker.getChapters(timeline, args.arc, args.episode);
14
+ }
15
+ else if (args.arc) {
16
+ const episodes = await tracker.getEpisodes(timeline, args.arc);
17
+ for (const ep of episodes) {
18
+ const epChapters = await tracker.getChapters(timeline, args.arc, ep.number);
19
+ chapters.push(...epChapters);
20
+ }
21
+ }
22
+ else {
23
+ const arcs = await tracker.getArcs(timeline);
24
+ for (const arc of arcs) {
25
+ const episodes = await tracker.getEpisodes(timeline, arc.name);
26
+ for (const ep of episodes) {
27
+ const epChapters = await tracker.getChapters(timeline, arc.name, ep.number);
28
+ chapters.push(...epChapters);
29
+ }
30
+ }
31
+ }
32
+ // Convert to embedding format and add to RAG
33
+ const embeddingChapters = chapters.map((ch) => ({
34
+ id: `${ch.timelineName}-${ch.arcName}-${ch.episodeNumber}-${ch.number}`,
35
+ metadata: ch,
36
+ content: '', // Content will be loaded by RAG system if needed
37
+ }));
38
+ await rag.addChapters(embeddingChapters);
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: JSON.stringify({
44
+ indexed: embeddingChapters.length,
45
+ timeline,
46
+ arc: args.arc || 'all',
47
+ episode: args.episode || 'all',
48
+ }, null, 2),
49
+ },
50
+ ],
51
+ };
52
+ }
53
+ catch (error) {
54
+ throw new Error(`Failed to index chapters: ${error instanceof Error ? error.message : 'Unknown error'}`);
55
+ }
56
+ }
@@ -0,0 +1,24 @@
1
+ import type { RAGSystem } from '@echoes-io/rag';
2
+ import { z } from 'zod';
3
+ export declare const ragSearchSchema: z.ZodObject<{
4
+ query: z.ZodString;
5
+ arc: z.ZodOptional<z.ZodString>;
6
+ pov: z.ZodOptional<z.ZodString>;
7
+ maxResults: z.ZodOptional<z.ZodNumber>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ query: string;
10
+ arc?: string | undefined;
11
+ pov?: string | undefined;
12
+ maxResults?: number | undefined;
13
+ }, {
14
+ query: string;
15
+ arc?: string | undefined;
16
+ pov?: string | undefined;
17
+ maxResults?: number | undefined;
18
+ }>;
19
+ export declare function ragSearch(args: z.infer<typeof ragSearchSchema>, rag: RAGSystem): Promise<{
20
+ content: {
21
+ type: "text";
22
+ text: string;
23
+ }[];
24
+ }>;
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ import { getTimeline } from '../utils.js';
3
+ export const ragSearchSchema = z.object({
4
+ query: z.string().describe('Search query'),
5
+ arc: z.string().optional().describe('Filter by arc name'),
6
+ pov: z.string().optional().describe('Filter by POV character'),
7
+ maxResults: z.number().optional().describe('Maximum number of results (default: 10)'),
8
+ });
9
+ export async function ragSearch(args, rag) {
10
+ try {
11
+ const timeline = getTimeline();
12
+ const results = await rag.search(args.query, {
13
+ timeline,
14
+ arc: args.arc,
15
+ pov: args.pov,
16
+ maxResults: args.maxResults,
17
+ });
18
+ return {
19
+ content: [
20
+ {
21
+ type: 'text',
22
+ text: JSON.stringify({
23
+ query: args.query,
24
+ timeline,
25
+ filters: {
26
+ arc: args.arc || null,
27
+ pov: args.pov || null,
28
+ },
29
+ results: results.map((r) => ({
30
+ chapter: {
31
+ arc: r.metadata.arcName,
32
+ episode: r.metadata.episodeNumber,
33
+ chapter: r.metadata.number,
34
+ pov: r.metadata.pov,
35
+ title: r.metadata.title,
36
+ },
37
+ similarity: r.similarity,
38
+ preview: `${r.content.substring(0, 200)}...`,
39
+ })),
40
+ }, null, 2),
41
+ },
42
+ ],
43
+ };
44
+ }
45
+ catch (error) {
46
+ throw new Error(`Failed to search: ${error instanceof Error ? error.message : 'Unknown error'}`);
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@echoes-io/mcp-server",
3
3
  "type": "module",
4
- "version": "1.1.0",
4
+ "version": "1.3.0",
5
5
  "description": "Model Context Protocol server for AI integration with Echoes storytelling platform",
6
6
  "scripts": {
7
7
  "dev": "tsx cli/index.ts",
@@ -81,9 +81,11 @@
81
81
  "vitest": "^3.2.4"
82
82
  },
83
83
  "dependencies": {
84
- "@echoes-io/utils": "^1.1.1",
84
+ "@echoes-io/books-generator": "^1.0.0",
85
85
  "@echoes-io/models": "^1.0.0",
86
+ "@echoes-io/rag": "^1.1.0",
86
87
  "@echoes-io/tracker": "^1.0.0",
88
+ "@echoes-io/utils": "^1.1.1",
87
89
  "@modelcontextprotocol/sdk": "^1.0.0"
88
90
  }
89
91
  }