@echoes-io/mcp-server 1.1.0 → 1.2.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_CHROMA_URL": "./rag_data"
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_CHROMA_URL`: ChromaDB storage path. Default: `./rag_data`
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,19 @@ 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
+
91
111
  ## Development
92
112
 
93
113
  ### Scripts
@@ -128,7 +148,6 @@ npm run lint:fix
128
148
 
129
149
  ### Planned Features
130
150
  - **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
151
 
133
152
  ## License
134
153
 
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 { 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,21 @@ 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
+ },
69
85
  ],
70
86
  };
71
87
  });
@@ -90,6 +106,12 @@ export function createServer(tracker) {
90
106
  return await timelineSync(timelineSyncSchema.parse(args), tracker);
91
107
  case 'stats':
92
108
  return await stats(statsSchema.parse(args), tracker);
109
+ case 'rag-index':
110
+ return await ragIndex(ragIndexSchema.parse(args), tracker, rag);
111
+ case 'rag-search':
112
+ return await ragSearch(ragSearchSchema.parse(args), rag);
113
+ case 'rag-context':
114
+ return await ragContext(ragContextSchema.parse(args), rag);
93
115
  default:
94
116
  throw new Error(`Unknown tool: ${name}`);
95
117
  }
@@ -102,7 +124,16 @@ export async function runServer() {
102
124
  const tracker = new Tracker(dbPath);
103
125
  await tracker.init();
104
126
  console.error(`Tracker database initialized: ${dbPath}`);
105
- const server = createServer(tracker);
127
+ // Initialize RAG system
128
+ const chromaUrl = process.env.ECHOES_CHROMA_URL || (process.env.NODE_ENV === 'test' ? ':memory:' : './rag_data');
129
+ const provider = (process.env.ECHOES_RAG_PROVIDER || 'e5-small');
130
+ const rag = new RAGSystem({
131
+ provider,
132
+ chromaUrl,
133
+ geminiApiKey: process.env.ECHOES_GEMINI_API_KEY,
134
+ });
135
+ console.error(`RAG system initialized: ${chromaUrl} (provider: ${provider})`);
136
+ const server = createServer(tracker, rag);
106
137
  const transport = new StdioServerTransport();
107
138
  await server.connect(transport);
108
139
  console.error('Echoes MCP Server running on stdio');
@@ -4,6 +4,9 @@ export { chapterInsert, chapterInsertSchema } from './chapter-insert.js';
4
4
  export { chapterRefresh, chapterRefreshSchema } from './chapter-refresh.js';
5
5
  export { episodeInfo, episodeInfoSchema } from './episode-info.js';
6
6
  export { episodeUpdate, episodeUpdateSchema } from './episode-update.js';
7
+ export { ragContext, ragContextSchema } from './rag-context.js';
8
+ export { ragIndex, ragIndexSchema } from './rag-index.js';
9
+ export { ragSearch, ragSearchSchema } from './rag-search.js';
7
10
  export { stats, statsSchema } from './stats.js';
8
11
  export { timelineSync, timelineSyncSchema } from './timeline-sync.js';
9
12
  export { wordsCount, wordsCountSchema } from './words-count.js';
@@ -4,6 +4,9 @@ export { chapterInsert, chapterInsertSchema } from './chapter-insert.js';
4
4
  export { chapterRefresh, chapterRefreshSchema } from './chapter-refresh.js';
5
5
  export { episodeInfo, episodeInfoSchema } from './episode-info.js';
6
6
  export { episodeUpdate, episodeUpdateSchema } from './episode-update.js';
7
+ export { ragContext, ragContextSchema } from './rag-context.js';
8
+ export { ragIndex, ragIndexSchema } from './rag-index.js';
9
+ export { ragSearch, ragSearchSchema } from './rag-search.js';
7
10
  export { stats, statsSchema } from './stats.js';
8
11
  export { timelineSync, timelineSyncSchema } from './timeline-sync.js';
9
12
  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.2.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,10 @@
81
81
  "vitest": "^3.2.4"
82
82
  },
83
83
  "dependencies": {
84
- "@echoes-io/utils": "^1.1.1",
85
84
  "@echoes-io/models": "^1.0.0",
85
+ "@echoes-io/rag": "^1.0.0",
86
86
  "@echoes-io/tracker": "^1.0.0",
87
+ "@echoes-io/utils": "^1.1.1",
87
88
  "@modelcontextprotocol/sdk": "^1.0.0"
88
89
  }
89
90
  }