@echoes-io/mcp-server 1.3.3 → 1.4.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
@@ -15,10 +15,7 @@ Add to your MCP client configuration (e.g., `~/.config/q/mcp.json` for Amazon Q)
15
15
  "mcpServers": {
16
16
  "echoes": {
17
17
  "command": "npx",
18
- "args": ["-y", "@echoes-io/mcp-server"],
19
- "env": {
20
- "ECHOES_TIMELINE": "your-timeline-name"
21
- }
18
+ "args": ["-y", "@echoes-io/mcp-server"]
22
19
  }
23
20
  }
24
21
  }
@@ -38,7 +35,6 @@ Then configure:
38
35
  "echoes": {
39
36
  "command": "echoes-mcp-server",
40
37
  "env": {
41
- "ECHOES_TIMELINE": "your-timeline-name",
42
38
  "ECHOES_RAG_PROVIDER": "e5-small",
43
39
  "ECHOES_RAG_DB_PATH": "./rag_data.db"
44
40
  }
@@ -47,8 +43,6 @@ Then configure:
47
43
  }
48
44
  ```
49
45
 
50
- **Important:** The `ECHOES_TIMELINE` environment variable must be set to specify which timeline to work with. All tools operate on this timeline.
51
-
52
46
  **Optional RAG Configuration:**
53
47
  - `ECHOES_RAG_PROVIDER`: Embedding provider (`e5-small`, `e5-large`, or `gemini`). Default: `e5-small`
54
48
  - `ECHOES_GEMINI_API_KEY`: Required if using `gemini` provider
@@ -56,7 +50,7 @@ Then configure:
56
50
 
57
51
  ## Available Tools
58
52
 
59
- All tools operate on the timeline specified by the `ECHOES_TIMELINE` environment variable.
53
+ All tools require a `timeline` parameter to specify which timeline to operate on.
60
54
 
61
55
  ### Content Operations
62
56
  - **`words-count`** - Count words and text statistics in markdown files
@@ -69,7 +63,7 @@ All tools operate on the timeline specified by the `ECHOES_TIMELINE` environment
69
63
  - Input: `file` (path to chapter file)
70
64
 
71
65
  - **`chapter-insert`** - Insert new chapter with automatic renumbering
72
- - Input: `arc`, `episode`, `after`, `pov`, `title`, optional: `excerpt`, `location`, `outfit`, `kink`, `file`
66
+ - Input: `arc`, `episode`, `after`, `pov`, `title`, optional: `summary`, `location`, `outfit`, `kink`, `file`
73
67
 
74
68
  - **`chapter-delete`** - Delete chapter from database and optionally from filesystem
75
69
  - Input: `arc`, `episode`, `chapter`, optional: `file` (to delete from filesystem)
@@ -150,7 +144,7 @@ npm run lint:format
150
144
  - **Database**: SQLite via @echoes-io/tracker (singleton pattern)
151
145
  - **Validation**: Zod schemas for type-safe inputs
152
146
  - **Testing**: Comprehensive unit and integration tests
153
- - **Environment**: Uses `ECHOES_TIMELINE` env var for timeline context
147
+ - **Timeline Parameter**: All tools accept timeline as a required parameter
154
148
 
155
149
  ## License
156
150
 
@@ -1,15 +1,18 @@
1
1
  import { z } from 'zod';
2
2
  export declare const bookGenerateSchema: z.ZodObject<{
3
+ timeline: z.ZodString;
3
4
  contentPath: z.ZodString;
4
5
  outputPath: z.ZodString;
5
6
  episodes: z.ZodOptional<z.ZodString>;
6
7
  format: z.ZodOptional<z.ZodEnum<["a4", "a5"]>>;
7
8
  }, "strip", z.ZodTypeAny, {
9
+ timeline: string;
8
10
  contentPath: string;
9
11
  outputPath: string;
10
12
  episodes?: string | undefined;
11
13
  format?: "a4" | "a5" | undefined;
12
14
  }, {
15
+ timeline: string;
13
16
  contentPath: string;
14
17
  outputPath: string;
15
18
  episodes?: string | undefined;
@@ -1,7 +1,7 @@
1
1
  import { generateBook } from '@echoes-io/books-generator';
2
2
  import { z } from 'zod';
3
- import { getTimeline } from '../utils.js';
4
3
  export const bookGenerateSchema = z.object({
4
+ timeline: z.string().describe('Timeline name'),
5
5
  contentPath: z.string().describe('Path to timeline content folder'),
6
6
  outputPath: z.string().describe('Output PDF file path'),
7
7
  episodes: z.string().optional().describe('Comma-separated episode numbers (e.g., "1,2,3")'),
@@ -9,11 +9,10 @@ export const bookGenerateSchema = z.object({
9
9
  });
10
10
  export async function bookGenerate(args) {
11
11
  try {
12
- const timeline = getTimeline();
13
12
  await generateBook({
14
13
  contentPath: args.contentPath,
15
14
  outputPath: args.outputPath,
16
- timeline,
15
+ timeline: args.timeline,
17
16
  episodes: args.episodes,
18
17
  format: args.format || 'a4',
19
18
  });
@@ -23,7 +22,7 @@ export async function bookGenerate(args) {
23
22
  type: 'text',
24
23
  text: JSON.stringify({
25
24
  success: true,
26
- timeline,
25
+ timeline: args.timeline,
27
26
  outputPath: args.outputPath,
28
27
  episodes: args.episodes || 'all',
29
28
  format: args.format || 'a4',
@@ -1,16 +1,19 @@
1
1
  import type { Tracker } from '@echoes-io/tracker';
2
2
  import { z } from 'zod';
3
3
  export declare const chapterDeleteSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  arc: z.ZodString;
5
6
  episode: z.ZodNumber;
6
7
  chapter: z.ZodNumber;
7
8
  file: z.ZodOptional<z.ZodString>;
8
9
  }, "strip", z.ZodTypeAny, {
10
+ timeline: string;
9
11
  arc: string;
10
12
  episode: number;
11
13
  chapter: number;
12
14
  file?: string | undefined;
13
15
  }, {
16
+ timeline: string;
14
17
  arc: string;
15
18
  episode: number;
16
19
  chapter: number;
@@ -1,7 +1,7 @@
1
1
  import { unlinkSync } from 'node:fs';
2
2
  import { z } from 'zod';
3
- import { getTimeline } from '../utils.js';
4
3
  export const chapterDeleteSchema = z.object({
4
+ timeline: z.string().describe('Timeline name'),
5
5
  arc: z.string().describe('Arc name'),
6
6
  episode: z.number().describe('Episode number'),
7
7
  chapter: z.number().describe('Chapter number'),
@@ -9,12 +9,11 @@ export const chapterDeleteSchema = z.object({
9
9
  });
10
10
  export async function chapterDelete(args, tracker) {
11
11
  try {
12
- const timeline = getTimeline();
13
- const existing = await tracker.getChapter(timeline, args.arc, args.episode, args.chapter);
12
+ const existing = await tracker.getChapter(args.timeline, args.arc, args.episode, args.chapter);
14
13
  if (!existing) {
15
- throw new Error(`Chapter not found: ${timeline}/${args.arc}/ep${args.episode}/ch${args.chapter}`);
14
+ throw new Error(`Chapter not found: ${args.timeline}/${args.arc}/ep${args.episode}/ch${args.chapter}`);
16
15
  }
17
- await tracker.deleteChapter(timeline, args.arc, args.episode, args.chapter);
16
+ await tracker.deleteChapter(args.timeline, args.arc, args.episode, args.chapter);
18
17
  let fileDeleted = false;
19
18
  if (args.file) {
20
19
  unlinkSync(args.file);
@@ -25,7 +24,7 @@ export async function chapterDelete(args, tracker) {
25
24
  {
26
25
  type: 'text',
27
26
  text: JSON.stringify({
28
- timeline,
27
+ timeline: args.timeline,
29
28
  arc: args.arc,
30
29
  episode: args.episode,
31
30
  chapter: args.chapter,
@@ -1,14 +1,17 @@
1
1
  import type { Tracker } from '@echoes-io/tracker';
2
2
  import { z } from 'zod';
3
3
  export declare const chapterInfoSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  arc: z.ZodString;
5
6
  episode: z.ZodNumber;
6
7
  chapter: z.ZodNumber;
7
8
  }, "strip", z.ZodTypeAny, {
9
+ timeline: string;
8
10
  arc: string;
9
11
  episode: number;
10
12
  chapter: number;
11
13
  }, {
14
+ timeline: string;
12
15
  arc: string;
13
16
  episode: number;
14
17
  chapter: number;
@@ -1,23 +1,22 @@
1
1
  import { z } from 'zod';
2
- import { getTimeline } from '../utils.js';
3
2
  export const chapterInfoSchema = z.object({
3
+ timeline: z.string().describe('Timeline name'),
4
4
  arc: z.string().describe('Arc name'),
5
5
  episode: z.number().describe('Episode number'),
6
6
  chapter: z.number().describe('Chapter number'),
7
7
  });
8
8
  export async function chapterInfo(args, tracker) {
9
9
  try {
10
- const timeline = getTimeline();
11
- const chapter = await tracker.getChapter(timeline, args.arc, args.episode, args.chapter);
10
+ const chapter = await tracker.getChapter(args.timeline, args.arc, args.episode, args.chapter);
12
11
  if (!chapter) {
13
- throw new Error(`Chapter not found: ${timeline}/${args.arc}/ep${args.episode}/ch${args.chapter}`);
12
+ throw new Error(`Chapter not found: ${args.timeline}/${args.arc}/ep${args.episode}/ch${args.chapter}`);
14
13
  }
15
14
  return {
16
15
  content: [
17
16
  {
18
17
  type: 'text',
19
18
  text: JSON.stringify({
20
- timeline,
19
+ timeline: args.timeline,
21
20
  arc: args.arc,
22
21
  episode: args.episode,
23
22
  chapter: args.chapter,
@@ -25,7 +24,7 @@ export async function chapterInfo(args, tracker) {
25
24
  pov: chapter.pov,
26
25
  title: chapter.title,
27
26
  date: chapter.date,
28
- excerpt: chapter.excerpt,
27
+ summary: chapter.summary,
29
28
  location: chapter.location,
30
29
  outfit: chapter.outfit,
31
30
  kink: chapter.kink,
@@ -1,35 +1,38 @@
1
1
  import type { Tracker } from '@echoes-io/tracker';
2
2
  import { z } from 'zod';
3
3
  export declare const chapterInsertSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  arc: z.ZodString;
5
6
  episode: z.ZodNumber;
6
7
  after: z.ZodNumber;
7
8
  pov: z.ZodString;
8
9
  title: z.ZodString;
9
- excerpt: z.ZodOptional<z.ZodString>;
10
+ summary: z.ZodOptional<z.ZodString>;
10
11
  location: z.ZodOptional<z.ZodString>;
11
12
  outfit: z.ZodOptional<z.ZodString>;
12
13
  kink: z.ZodOptional<z.ZodString>;
13
14
  file: z.ZodOptional<z.ZodString>;
14
15
  }, "strip", z.ZodTypeAny, {
16
+ timeline: string;
15
17
  arc: string;
16
18
  episode: number;
17
19
  after: number;
18
20
  pov: string;
19
21
  title: string;
20
22
  file?: string | undefined;
21
- excerpt?: string | undefined;
23
+ summary?: string | undefined;
22
24
  location?: string | undefined;
23
25
  outfit?: string | undefined;
24
26
  kink?: string | undefined;
25
27
  }, {
28
+ timeline: string;
26
29
  arc: string;
27
30
  episode: number;
28
31
  after: number;
29
32
  pov: string;
30
33
  title: string;
31
34
  file?: string | undefined;
32
- excerpt?: string | undefined;
35
+ summary?: string | undefined;
33
36
  location?: string | undefined;
34
37
  outfit?: string | undefined;
35
38
  kink?: string | undefined;
@@ -1,13 +1,13 @@
1
1
  import { getTextStats, parseMarkdown } from '@echoes-io/utils';
2
2
  import { z } from 'zod';
3
- import { getTimeline } from '../utils.js';
4
3
  export const chapterInsertSchema = z.object({
4
+ timeline: z.string().describe('Timeline name'),
5
5
  arc: z.string().describe('Arc name'),
6
6
  episode: z.number().describe('Episode number'),
7
7
  after: z.number().describe('Insert after this chapter number'),
8
8
  pov: z.string().describe('Point of view character'),
9
9
  title: z.string().describe('Chapter title'),
10
- excerpt: z.string().optional().describe('Chapter excerpt'),
10
+ summary: z.string().optional().describe('Chapter summary'),
11
11
  location: z.string().optional().describe('Chapter location'),
12
12
  outfit: z.string().optional().describe('Character outfit'),
13
13
  kink: z.string().optional().describe('Content tags'),
@@ -15,17 +15,16 @@ export const chapterInsertSchema = z.object({
15
15
  });
16
16
  export async function chapterInsert(args, tracker) {
17
17
  try {
18
- const timeline = getTimeline();
19
- const episode = await tracker.getEpisode(timeline, args.arc, args.episode);
18
+ const episode = await tracker.getEpisode(args.timeline, args.arc, args.episode);
20
19
  if (!episode) {
21
- throw new Error(`Episode not found: ${timeline}/${args.arc}/ep${args.episode}`);
20
+ throw new Error(`Episode not found: ${args.timeline}/${args.arc}/ep${args.episode}`);
22
21
  }
23
- const existingChapters = await tracker.getChapters(timeline, args.arc, args.episode);
22
+ const existingChapters = await tracker.getChapters(args.timeline, args.arc, args.episode);
24
23
  const newChapterNumber = args.after + 1;
25
24
  const chaptersToRenumber = existingChapters.filter((ch) => ch.number >= newChapterNumber);
26
25
  for (const chapter of chaptersToRenumber) {
27
26
  try {
28
- await tracker.updateChapter(timeline, args.arc, args.episode, chapter.number, {
27
+ await tracker.updateChapter(args.timeline, args.arc, args.episode, chapter.number, {
29
28
  number: chapter.number + 1,
30
29
  });
31
30
  }
@@ -49,16 +48,21 @@ export async function chapterInsert(args, tracker) {
49
48
  paragraphs = stats.paragraphs;
50
49
  sentences = stats.sentences;
51
50
  }
52
- await tracker.createChapter({
53
- timelineName: timeline,
51
+ const chapterData = {
52
+ timelineName: args.timeline,
54
53
  arcName: args.arc,
55
54
  episodeNumber: args.episode,
56
55
  partNumber: 1,
57
56
  number: newChapterNumber,
57
+ timeline: args.timeline,
58
+ arc: args.arc,
59
+ episode: args.episode,
60
+ part: 1,
61
+ chapter: newChapterNumber,
58
62
  pov: args.pov,
59
63
  title: args.title,
60
- date: new Date(),
61
- excerpt: args.excerpt || '',
64
+ date: new Date().toISOString(),
65
+ summary: args.summary || '',
62
66
  location: args.location || '',
63
67
  outfit: args.outfit || '',
64
68
  kink: args.kink || '',
@@ -68,13 +72,14 @@ export async function chapterInsert(args, tracker) {
68
72
  paragraphs,
69
73
  sentences,
70
74
  readingTimeMinutes: Math.ceil(words / 200),
71
- });
75
+ };
76
+ await tracker.createChapter(chapterData);
72
77
  return {
73
78
  content: [
74
79
  {
75
80
  type: 'text',
76
81
  text: JSON.stringify({
77
- timeline,
82
+ timeline: args.timeline,
78
83
  arc: args.arc,
79
84
  episode: args.episode,
80
85
  inserted: {
@@ -1,10 +1,13 @@
1
1
  import type { Tracker } from '@echoes-io/tracker';
2
2
  import { z } from 'zod';
3
3
  export declare const chapterRefreshSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  file: z.ZodString;
5
6
  }, "strip", z.ZodTypeAny, {
7
+ timeline: string;
6
8
  file: string;
7
9
  }, {
10
+ timeline: string;
8
11
  file: string;
9
12
  }>;
10
13
  export declare function chapterRefresh(args: z.infer<typeof chapterRefreshSchema>, tracker: Tracker): Promise<{
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { getTextStats, parseMarkdown } from '@echoes-io/utils';
3
3
  import { z } from 'zod';
4
- import { getTimeline } from '../utils.js';
5
4
  export const chapterRefreshSchema = z.object({
5
+ timeline: z.string().describe('Timeline name'),
6
6
  file: z.string().describe('Path to chapter markdown file'),
7
7
  });
8
8
  export async function chapterRefresh(args, tracker) {
@@ -10,27 +10,26 @@ export async function chapterRefresh(args, tracker) {
10
10
  const content = readFileSync(args.file, 'utf-8');
11
11
  const { metadata, content: markdownContent } = parseMarkdown(content);
12
12
  const stats = getTextStats(markdownContent);
13
- const timeline = getTimeline();
14
13
  const arc = metadata.arc;
15
14
  const episode = metadata.episode;
16
15
  const chapter = metadata.chapter;
17
16
  if (!arc || !episode || !chapter) {
18
17
  throw new Error('Missing required metadata: arc, episode, or chapter');
19
18
  }
20
- const existing = await tracker.getChapter(timeline, arc, episode, chapter);
19
+ const existing = await tracker.getChapter(args.timeline, arc, episode, chapter);
21
20
  if (!existing) {
22
- throw new Error(`Chapter not found in database: ${timeline}/${arc}/ep${episode}/ch${chapter}`);
21
+ throw new Error(`Chapter not found in database: ${args.timeline}/${arc}/ep${episode}/ch${chapter}`);
23
22
  }
24
23
  const chapterData = {
25
- timelineName: timeline,
24
+ timelineName: args.timeline,
26
25
  arcName: arc,
27
26
  episodeNumber: episode,
28
27
  partNumber: metadata.part || 1,
29
28
  number: chapter,
30
29
  pov: metadata.pov || 'Unknown',
31
30
  title: metadata.title || 'Untitled',
32
- date: new Date(metadata.date || Date.now()),
33
- excerpt: metadata.excerpt || '',
31
+ date: new Date(metadata.date || Date.now()).toISOString(),
32
+ summary: metadata.summary || '',
34
33
  location: metadata.location || '',
35
34
  outfit: metadata.outfit || '',
36
35
  kink: metadata.kink || '',
@@ -41,14 +40,14 @@ export async function chapterRefresh(args, tracker) {
41
40
  sentences: stats.sentences,
42
41
  readingTimeMinutes: Math.ceil(stats.words / 200),
43
42
  };
44
- await tracker.updateChapter(timeline, arc, episode, chapter, chapterData);
43
+ await tracker.updateChapter(args.timeline, arc, episode, chapter, chapterData);
45
44
  return {
46
45
  content: [
47
46
  {
48
47
  type: 'text',
49
48
  text: JSON.stringify({
50
49
  file: args.file,
51
- timeline,
50
+ timeline: args.timeline,
52
51
  arc,
53
52
  episode,
54
53
  chapter,
@@ -57,7 +56,7 @@ export async function chapterRefresh(args, tracker) {
57
56
  pov: chapterData.pov,
58
57
  title: chapterData.title,
59
58
  date: chapterData.date,
60
- excerpt: chapterData.excerpt,
59
+ summary: chapterData.summary,
61
60
  location: chapterData.location,
62
61
  },
63
62
  stats: {
@@ -1,12 +1,15 @@
1
1
  import type { Tracker } from '@echoes-io/tracker';
2
2
  import { z } from 'zod';
3
3
  export declare const episodeInfoSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  arc: z.ZodString;
5
6
  episode: z.ZodNumber;
6
7
  }, "strip", z.ZodTypeAny, {
8
+ timeline: string;
7
9
  arc: string;
8
10
  episode: number;
9
11
  }, {
12
+ timeline: string;
10
13
  arc: string;
11
14
  episode: number;
12
15
  }>;
@@ -1,23 +1,22 @@
1
1
  import { z } from 'zod';
2
- import { getTimeline } from '../utils.js';
3
2
  export const episodeInfoSchema = z.object({
3
+ timeline: z.string().describe('Timeline name'),
4
4
  arc: z.string().describe('Arc name'),
5
5
  episode: z.number().describe('Episode number'),
6
6
  });
7
7
  export async function episodeInfo(args, tracker) {
8
8
  try {
9
- const timeline = getTimeline();
10
- const episode = await tracker.getEpisode(timeline, args.arc, args.episode);
9
+ const episode = await tracker.getEpisode(args.timeline, args.arc, args.episode);
11
10
  if (!episode) {
12
- throw new Error(`Episode not found: ${timeline}/${args.arc}/ep${args.episode}`);
11
+ throw new Error(`Episode not found: ${args.timeline}/${args.arc}/ep${args.episode}`);
13
12
  }
14
- const chapters = await tracker.getChapters(timeline, args.arc, args.episode);
13
+ const chapters = await tracker.getChapters(args.timeline, args.arc, args.episode);
15
14
  return {
16
15
  content: [
17
16
  {
18
17
  type: 'text',
19
18
  text: JSON.stringify({
20
- timeline,
19
+ timeline: args.timeline,
21
20
  arc: args.arc,
22
21
  episodeInfo: {
23
22
  number: episode.number,
@@ -1,18 +1,21 @@
1
1
  import type { Tracker } from '@echoes-io/tracker';
2
2
  import { z } from 'zod';
3
3
  export declare const episodeUpdateSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  arc: z.ZodString;
5
6
  episode: z.ZodNumber;
6
7
  description: z.ZodOptional<z.ZodString>;
7
8
  title: z.ZodOptional<z.ZodString>;
8
9
  slug: z.ZodOptional<z.ZodString>;
9
10
  }, "strip", z.ZodTypeAny, {
11
+ timeline: string;
10
12
  arc: string;
11
13
  episode: number;
12
14
  title?: string | undefined;
13
15
  description?: string | undefined;
14
16
  slug?: string | undefined;
15
17
  }, {
18
+ timeline: string;
16
19
  arc: string;
17
20
  episode: number;
18
21
  title?: string | undefined;
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { getTimeline } from '../utils.js';
3
2
  export const episodeUpdateSchema = z.object({
3
+ timeline: z.string().describe('Timeline name'),
4
4
  arc: z.string().describe('Arc name'),
5
5
  episode: z.number().describe('Episode number'),
6
6
  description: z.string().optional().describe('Episode description'),
@@ -9,10 +9,9 @@ export const episodeUpdateSchema = z.object({
9
9
  });
10
10
  export async function episodeUpdate(args, tracker) {
11
11
  try {
12
- const timeline = getTimeline();
13
- const existing = await tracker.getEpisode(timeline, args.arc, args.episode);
12
+ const existing = await tracker.getEpisode(args.timeline, args.arc, args.episode);
14
13
  if (!existing) {
15
- throw new Error(`Episode not found: ${timeline}/${args.arc}/ep${args.episode}`);
14
+ throw new Error(`Episode not found: ${args.timeline}/${args.arc}/ep${args.episode}`);
16
15
  }
17
16
  const updateData = {};
18
17
  if (args.description !== undefined)
@@ -21,13 +20,13 @@ export async function episodeUpdate(args, tracker) {
21
20
  updateData.title = args.title;
22
21
  if (args.slug !== undefined)
23
22
  updateData.slug = args.slug;
24
- await tracker.updateEpisode(timeline, args.arc, args.episode, updateData);
23
+ await tracker.updateEpisode(args.timeline, args.arc, args.episode, updateData);
25
24
  return {
26
25
  content: [
27
26
  {
28
27
  type: 'text',
29
28
  text: JSON.stringify({
30
- timeline,
29
+ timeline: args.timeline,
31
30
  arc: args.arc,
32
31
  episode: args.episode,
33
32
  updated: updateData,
@@ -1,16 +1,19 @@
1
1
  import type { RAGSystem } from '@echoes-io/rag';
2
2
  import { z } from 'zod';
3
3
  export declare const ragContextSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  query: z.ZodString;
5
6
  arc: z.ZodOptional<z.ZodString>;
6
7
  pov: z.ZodOptional<z.ZodString>;
7
8
  maxChapters: z.ZodOptional<z.ZodNumber>;
8
9
  }, "strip", z.ZodTypeAny, {
10
+ timeline: string;
9
11
  query: string;
10
12
  arc?: string | undefined;
11
13
  pov?: string | undefined;
12
14
  maxChapters?: number | undefined;
13
15
  }, {
16
+ timeline: string;
14
17
  query: string;
15
18
  arc?: string | undefined;
16
19
  pov?: string | undefined;
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { getTimeline } from '../utils.js';
3
2
  export const ragContextSchema = z.object({
3
+ timeline: z.string().describe('Timeline name'),
4
4
  query: z.string().describe('Context query'),
5
5
  arc: z.string().optional().describe('Filter by arc name'),
6
6
  pov: z.string().optional().describe('Filter by POV character'),
@@ -8,10 +8,9 @@ export const ragContextSchema = z.object({
8
8
  });
9
9
  export async function ragContext(args, rag) {
10
10
  try {
11
- const timeline = getTimeline();
12
11
  const results = await rag.getContext({
13
12
  query: args.query,
14
- timeline,
13
+ timeline: args.timeline,
15
14
  arc: args.arc,
16
15
  pov: args.pov,
17
16
  maxChapters: args.maxChapters,
@@ -22,7 +21,7 @@ export async function ragContext(args, rag) {
22
21
  type: 'text',
23
22
  text: JSON.stringify({
24
23
  query: args.query,
25
- timeline,
24
+ timeline: args.timeline,
26
25
  filters: {
27
26
  arc: args.arc || null,
28
27
  pov: args.pov || null,
@@ -2,14 +2,17 @@ import type { RAGSystem } from '@echoes-io/rag';
2
2
  import type { Tracker } from '@echoes-io/tracker';
3
3
  import { z } from 'zod';
4
4
  export declare const ragIndexSchema: z.ZodObject<{
5
+ timeline: z.ZodString;
5
6
  contentPath: z.ZodOptional<z.ZodString>;
6
7
  arc: z.ZodOptional<z.ZodString>;
7
8
  episode: z.ZodOptional<z.ZodNumber>;
8
9
  }, "strip", z.ZodTypeAny, {
10
+ timeline: string;
9
11
  contentPath?: string | undefined;
10
12
  arc?: string | undefined;
11
13
  episode?: number | undefined;
12
14
  }, {
15
+ timeline: string;
13
16
  contentPath?: string | undefined;
14
17
  arc?: string | undefined;
15
18
  episode?: number | undefined;
@@ -2,33 +2,32 @@ import { readdirSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { parseMarkdown } from '@echoes-io/utils';
4
4
  import { z } from 'zod';
5
- import { getTimeline } from '../utils.js';
6
5
  export const ragIndexSchema = z.object({
6
+ timeline: z.string().describe('Timeline name'),
7
7
  contentPath: z.string().optional().describe('Path to content directory (required for indexing)'),
8
8
  arc: z.string().optional().describe('Index specific arc only'),
9
9
  episode: z.number().optional().describe('Index specific episode only (requires arc)'),
10
10
  });
11
11
  export async function ragIndex(args, tracker, rag) {
12
12
  try {
13
- const timeline = getTimeline();
14
13
  let chapters = [];
15
14
  // Get chapters based on filters
16
15
  if (args.arc && args.episode) {
17
- chapters = await tracker.getChapters(timeline, args.arc, args.episode);
16
+ chapters = await tracker.getChapters(args.timeline, args.arc, args.episode);
18
17
  }
19
18
  else if (args.arc) {
20
- const episodes = await tracker.getEpisodes(timeline, args.arc);
19
+ const episodes = await tracker.getEpisodes(args.timeline, args.arc);
21
20
  for (const ep of episodes) {
22
- const epChapters = await tracker.getChapters(timeline, args.arc, ep.number);
21
+ const epChapters = await tracker.getChapters(args.timeline, args.arc, ep.number);
23
22
  chapters.push(...epChapters);
24
23
  }
25
24
  }
26
25
  else {
27
- const arcs = await tracker.getArcs(timeline);
26
+ const arcs = await tracker.getArcs(args.timeline);
28
27
  for (const arc of arcs) {
29
- const episodes = await tracker.getEpisodes(timeline, arc.name);
28
+ const episodes = await tracker.getEpisodes(args.timeline, arc.name);
30
29
  for (const ep of episodes) {
31
- const epChapters = await tracker.getChapters(timeline, arc.name, ep.number);
30
+ const epChapters = await tracker.getChapters(args.timeline, arc.name, ep.number);
32
31
  chapters.push(...epChapters);
33
32
  }
34
33
  }
@@ -85,7 +84,7 @@ export async function ragIndex(args, tracker, rag) {
85
84
  type: 'text',
86
85
  text: JSON.stringify({
87
86
  indexed: embeddingChapters.length,
88
- timeline,
87
+ timeline: args.timeline,
89
88
  arc: args.arc || 'all',
90
89
  episode: args.episode || 'all',
91
90
  }, null, 2),
@@ -1,16 +1,19 @@
1
1
  import type { RAGSystem } from '@echoes-io/rag';
2
2
  import { z } from 'zod';
3
3
  export declare const ragSearchSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  query: z.ZodString;
5
6
  arc: z.ZodOptional<z.ZodString>;
6
7
  pov: z.ZodOptional<z.ZodString>;
7
8
  maxResults: z.ZodOptional<z.ZodNumber>;
8
9
  }, "strip", z.ZodTypeAny, {
10
+ timeline: string;
9
11
  query: string;
10
12
  arc?: string | undefined;
11
13
  pov?: string | undefined;
12
14
  maxResults?: number | undefined;
13
15
  }, {
16
+ timeline: string;
14
17
  query: string;
15
18
  arc?: string | undefined;
16
19
  pov?: string | undefined;
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { getTimeline } from '../utils.js';
3
2
  export const ragSearchSchema = z.object({
3
+ timeline: z.string().describe('Timeline name'),
4
4
  query: z.string().describe('Search query'),
5
5
  arc: z.string().optional().describe('Filter by arc name'),
6
6
  pov: z.string().optional().describe('Filter by POV character'),
@@ -8,9 +8,8 @@ export const ragSearchSchema = z.object({
8
8
  });
9
9
  export async function ragSearch(args, rag) {
10
10
  try {
11
- const timeline = getTimeline();
12
11
  const results = await rag.search(args.query, {
13
- timeline,
12
+ timeline: args.timeline,
14
13
  arc: args.arc,
15
14
  pov: args.pov,
16
15
  maxResults: args.maxResults,
@@ -21,7 +20,7 @@ export async function ragSearch(args, rag) {
21
20
  type: 'text',
22
21
  text: JSON.stringify({
23
22
  query: args.query,
24
- timeline,
23
+ timeline: args.timeline,
25
24
  filters: {
26
25
  arc: args.arc || null,
27
26
  pov: args.pov || null,
@@ -1,14 +1,17 @@
1
1
  import type { Tracker } from '@echoes-io/tracker';
2
2
  import { z } from 'zod';
3
3
  export declare const statsSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  arc: z.ZodOptional<z.ZodString>;
5
6
  episode: z.ZodOptional<z.ZodNumber>;
6
7
  pov: z.ZodOptional<z.ZodString>;
7
8
  }, "strip", z.ZodTypeAny, {
9
+ timeline: string;
8
10
  arc?: string | undefined;
9
11
  episode?: number | undefined;
10
12
  pov?: string | undefined;
11
13
  }, {
14
+ timeline: string;
12
15
  arc?: string | undefined;
13
16
  episode?: number | undefined;
14
17
  pov?: string | undefined;
@@ -1,31 +1,30 @@
1
1
  import { z } from 'zod';
2
- import { getTimeline } from '../utils.js';
3
2
  export const statsSchema = z.object({
3
+ timeline: z.string().describe('Timeline name'),
4
4
  arc: z.string().optional().describe('Filter by arc name'),
5
5
  episode: z.number().optional().describe('Filter by episode number'),
6
6
  pov: z.string().optional().describe('Filter by POV character'),
7
7
  });
8
8
  export async function stats(args, tracker) {
9
9
  try {
10
- const timeline = getTimeline();
11
10
  let chapters = [];
12
11
  // Get chapters based on filters
13
12
  if (args.arc && args.episode) {
14
- chapters = await tracker.getChapters(timeline, args.arc, args.episode);
13
+ chapters = await tracker.getChapters(args.timeline, args.arc, args.episode);
15
14
  }
16
15
  else if (args.arc) {
17
- const episodes = await tracker.getEpisodes(timeline, args.arc);
16
+ const episodes = await tracker.getEpisodes(args.timeline, args.arc);
18
17
  for (const ep of episodes) {
19
- const epChapters = await tracker.getChapters(timeline, args.arc, ep.number);
18
+ const epChapters = await tracker.getChapters(args.timeline, args.arc, ep.number);
20
19
  chapters.push(...epChapters);
21
20
  }
22
21
  }
23
22
  else {
24
- const arcs = await tracker.getArcs(timeline);
23
+ const arcs = await tracker.getArcs(args.timeline);
25
24
  for (const arc of arcs) {
26
- const episodes = await tracker.getEpisodes(timeline, arc.name);
25
+ const episodes = await tracker.getEpisodes(args.timeline, arc.name);
27
26
  for (const ep of episodes) {
28
- const epChapters = await tracker.getChapters(timeline, arc.name, ep.number);
27
+ const epChapters = await tracker.getChapters(args.timeline, arc.name, ep.number);
29
28
  chapters.push(...epChapters);
30
29
  }
31
30
  }
@@ -70,7 +69,7 @@ export async function stats(args, tracker) {
70
69
  }
71
70
  }
72
71
  const result = {
73
- timeline,
72
+ timeline: args.timeline,
74
73
  filters: {
75
74
  arc: args.arc || null,
76
75
  episode: args.episode || null,
@@ -1,10 +1,13 @@
1
1
  import type { Tracker } from '@echoes-io/tracker';
2
2
  import { z } from 'zod';
3
3
  export declare const timelineSyncSchema: z.ZodObject<{
4
+ timeline: z.ZodString;
4
5
  contentPath: z.ZodString;
5
6
  }, "strip", z.ZodTypeAny, {
7
+ timeline: string;
6
8
  contentPath: string;
7
9
  }, {
10
+ timeline: string;
8
11
  contentPath: string;
9
12
  }>;
10
13
  export declare function timelineSync(args: z.infer<typeof timelineSyncSchema>, tracker: Tracker): Promise<{
@@ -2,19 +2,18 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { extname, join } from 'node:path';
3
3
  import { getTextStats, parseMarkdown } from '@echoes-io/utils';
4
4
  import { z } from 'zod';
5
- import { getTimeline } from '../utils.js';
6
5
  export const timelineSyncSchema = z.object({
6
+ timeline: z.string().describe('Timeline name'),
7
7
  contentPath: z.string().describe('Path to content directory'),
8
8
  });
9
9
  export async function timelineSync(args, tracker) {
10
10
  try {
11
- const timeline = getTimeline();
12
11
  let added = 0, updated = 0, deleted = 0, errors = 0;
13
- let timelineRecord = await tracker.getTimeline(timeline);
12
+ let timelineRecord = await tracker.getTimeline(args.timeline);
14
13
  if (!timelineRecord) {
15
14
  timelineRecord = await tracker.createTimeline({
16
- name: timeline,
17
- description: `Timeline ${timeline}`,
15
+ name: args.timeline,
16
+ description: `Timeline ${args.timeline}`,
18
17
  });
19
18
  added++;
20
19
  }
@@ -23,10 +22,10 @@ export async function timelineSync(args, tracker) {
23
22
  .map((entry) => entry.name);
24
23
  for (const arcName of arcs) {
25
24
  const arcPath = join(args.contentPath, arcName);
26
- let arc = await tracker.getArc(timeline, arcName);
25
+ let arc = await tracker.getArc(args.timeline, arcName);
27
26
  if (!arc) {
28
27
  arc = await tracker.createArc({
29
- timelineName: timeline,
28
+ timelineName: args.timeline,
30
29
  name: arcName,
31
30
  number: 1,
32
31
  description: `Arc ${arcName}`,
@@ -41,11 +40,11 @@ export async function timelineSync(args, tracker) {
41
40
  }));
42
41
  for (const ep of episodes) {
43
42
  const episodePath = join(arcPath, ep.name);
44
- let episode = await tracker.getEpisode(timeline, arcName, ep.number);
43
+ let episode = await tracker.getEpisode(args.timeline, arcName, ep.number);
45
44
  if (!episode) {
46
45
  try {
47
46
  episode = await tracker.createEpisode({
48
- timelineName: timeline,
47
+ timelineName: args.timeline,
49
48
  arcName: arcName,
50
49
  number: ep.number,
51
50
  slug: ep.name,
@@ -57,7 +56,7 @@ export async function timelineSync(args, tracker) {
57
56
  catch (error) {
58
57
  console.error(`Error creating episode ${arcName}/ep${ep.number}:`, error instanceof Error ? error.message : error);
59
58
  errors++;
60
- continue; // Skip chapters if episode creation failed
59
+ continue;
61
60
  }
62
61
  }
63
62
  const chapters = readdirSync(episodePath)
@@ -80,15 +79,13 @@ export async function timelineSync(args, tracker) {
80
79
  }
81
80
  })
82
81
  .filter((ch) => ch !== null);
83
- // Collect unique part numbers
84
82
  const partNumbers = new Set(chapters.map((ch) => ch?.metadata.part || 1));
85
- // Create parts if they don't exist
86
83
  for (const partNum of partNumbers) {
87
84
  try {
88
- const existingPart = await tracker.getPart(timeline, arcName, ep.number, partNum);
85
+ const existingPart = await tracker.getPart(args.timeline, arcName, ep.number, partNum);
89
86
  if (!existingPart) {
90
87
  await tracker.createPart({
91
- timelineName: timeline,
88
+ timelineName: args.timeline,
92
89
  arcName: arcName,
93
90
  episodeNumber: ep.number,
94
91
  number: partNum,
@@ -111,17 +108,22 @@ export async function timelineSync(args, tracker) {
111
108
  if (!chNumber)
112
109
  continue;
113
110
  try {
114
- const existing = await tracker.getChapter(timeline, arcName, ep.number, chNumber);
111
+ const existing = await tracker.getChapter(args.timeline, arcName, ep.number, chNumber);
115
112
  const data = {
116
- timelineName: timeline,
113
+ timelineName: args.timeline,
117
114
  arcName: arcName,
118
115
  episodeNumber: ep.number,
119
116
  partNumber: chapterData.metadata.part || 1,
120
117
  number: chNumber,
118
+ timeline: args.timeline,
119
+ arc: arcName,
120
+ episode: ep.number,
121
+ part: chapterData.metadata.part || 1,
122
+ chapter: chNumber,
121
123
  pov: chapterData.metadata.pov || 'Unknown',
122
124
  title: chapterData.metadata.title || 'Untitled',
123
- date: new Date(chapterData.metadata.date || Date.now()),
124
- excerpt: chapterData.metadata.excerpt || '',
125
+ date: new Date(chapterData.metadata.date || Date.now()).toISOString(),
126
+ summary: chapterData.metadata.summary || '',
125
127
  location: chapterData.metadata.location || '',
126
128
  outfit: chapterData.metadata.outfit || '',
127
129
  kink: chapterData.metadata.kink || '',
@@ -133,7 +135,7 @@ export async function timelineSync(args, tracker) {
133
135
  readingTimeMinutes: Math.ceil(chapterData.stats.words / 200),
134
136
  };
135
137
  if (existing) {
136
- await tracker.updateChapter(timeline, arcName, ep.number, chNumber, data);
138
+ await tracker.updateChapter(args.timeline, arcName, ep.number, chNumber, data);
137
139
  updated++;
138
140
  }
139
141
  else {
@@ -148,11 +150,11 @@ export async function timelineSync(args, tracker) {
148
150
  }
149
151
  }
150
152
  try {
151
- const dbArcs = await tracker.getArcs(timeline);
153
+ const dbArcs = await tracker.getArcs(args.timeline);
152
154
  for (const arc of dbArcs) {
153
- const dbEpisodes = await tracker.getEpisodes(timeline, arc.name);
155
+ const dbEpisodes = await tracker.getEpisodes(args.timeline, arc.name);
154
156
  for (const episode of dbEpisodes) {
155
- const allChapters = await tracker.getChapters(timeline, arc.name, episode.number);
157
+ const allChapters = await tracker.getChapters(args.timeline, arc.name, episode.number);
156
158
  for (const dbChapter of allChapters) {
157
159
  let fileExists = false;
158
160
  try {
@@ -195,7 +197,7 @@ export async function timelineSync(args, tracker) {
195
197
  {
196
198
  type: 'text',
197
199
  text: JSON.stringify({
198
- timeline,
200
+ timeline: args.timeline,
199
201
  contentPath: args.contentPath,
200
202
  summary: {
201
203
  added,
package/lib/utils.d.ts CHANGED
@@ -1 +1 @@
1
- export declare function getTimeline(): string;
1
+ export {};
package/lib/utils.js CHANGED
@@ -1,7 +1,2 @@
1
- export function getTimeline() {
2
- const timeline = process.env.ECHOES_TIMELINE;
3
- if (!timeline) {
4
- throw new Error('ECHOES_TIMELINE environment variable is not set');
5
- }
6
- return timeline;
7
- }
1
+ export {};
2
+ // Removed getTimeline() - timeline is now a required parameter for all tools
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@echoes-io/mcp-server",
3
3
  "type": "module",
4
- "version": "1.3.3",
4
+ "version": "1.4.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",
@@ -13,6 +13,7 @@
13
13
  "lint:publish": "publint --strict",
14
14
  "test": "vitest run",
15
15
  "test:coverage": "vitest run --coverage",
16
+ "check": "npm run clean && npm run test:coverage && npm run build && npm run lint && npm run clean",
16
17
  "prepare": "[ \"$CI\" = \"true\" ] || [ \"$GITHUB_ACTIONS\" = \"true\" ] && echo 'Skipping husky' && exit 0 || husky",
17
18
  "clean": "rimraf --glob ./{cli,lib,test}/**/*.{d.ts,js} ./vitest*.{d.ts,js}",
18
19
  "prebuild": "npm run clean",
@@ -62,7 +63,7 @@
62
63
  ]
63
64
  },
64
65
  "devDependencies": {
65
- "@biomejs/biome": "^2.2.7",
66
+ "@biomejs/biome": "^2.3.2",
66
67
  "@semantic-release/changelog": "^6.0.3",
67
68
  "@semantic-release/git": "^10.0.1",
68
69
  "@tsconfig/node22": "^22.0.2",
@@ -81,11 +82,11 @@
81
82
  "vitest": "^3.2.4"
82
83
  },
83
84
  "dependencies": {
84
- "@echoes-io/books-generator": "^1.0.0",
85
- "@echoes-io/models": "^1.0.1",
85
+ "@echoes-io/books-generator": "^1.0.1",
86
+ "@echoes-io/models": "^1.0.2",
86
87
  "@echoes-io/rag": "^1.1.2",
87
- "@echoes-io/tracker": "^1.0.0",
88
- "@echoes-io/utils": "^1.1.1",
88
+ "@echoes-io/tracker": "^1.0.1",
89
+ "@echoes-io/utils": "^1.2.0",
89
90
  "@modelcontextprotocol/sdk": "^1.0.0"
90
91
  }
91
92
  }