@echoes-io/mcp-server 1.0.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.
@@ -0,0 +1,183 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { extname, join } from 'node:path';
3
+ import { getTextStats, parseMarkdown } from '@echoes-io/utils';
4
+ import { z } from 'zod';
5
+ import { getTimeline } from '../utils.js';
6
+ export const timelineSyncSchema = z.object({
7
+ contentPath: z.string().describe('Path to content directory'),
8
+ });
9
+ export async function timelineSync(args, tracker) {
10
+ try {
11
+ const timeline = getTimeline();
12
+ let added = 0, updated = 0, deleted = 0, errors = 0;
13
+ let timelineRecord = await tracker.getTimeline(timeline);
14
+ if (!timelineRecord) {
15
+ timelineRecord = await tracker.createTimeline({
16
+ name: timeline,
17
+ description: `Timeline ${timeline}`,
18
+ });
19
+ added++;
20
+ }
21
+ const arcs = readdirSync(args.contentPath, { withFileTypes: true })
22
+ .filter((entry) => entry.isDirectory())
23
+ .map((entry) => entry.name);
24
+ for (const arcName of arcs) {
25
+ const arcPath = join(args.contentPath, arcName);
26
+ let arc = await tracker.getArc(timeline, arcName);
27
+ if (!arc) {
28
+ arc = await tracker.createArc({
29
+ timelineName: timeline,
30
+ name: arcName,
31
+ number: 1,
32
+ description: `Arc ${arcName}`,
33
+ });
34
+ added++;
35
+ }
36
+ const episodes = readdirSync(arcPath, { withFileTypes: true })
37
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('ep'))
38
+ .map((entry) => ({
39
+ name: entry.name,
40
+ number: Number.parseInt(entry.name.match(/ep(\d+)/)?.[1] || '0', 10),
41
+ }));
42
+ for (const ep of episodes) {
43
+ const episodePath = join(arcPath, ep.name);
44
+ let episode = await tracker.getEpisode(timeline, arcName, ep.number);
45
+ if (!episode) {
46
+ episode = await tracker.createEpisode({
47
+ timelineName: timeline,
48
+ arcName: arcName,
49
+ number: ep.number,
50
+ slug: ep.name,
51
+ title: ep.name,
52
+ description: `Episode ${ep.number}`,
53
+ });
54
+ added++;
55
+ }
56
+ const chapters = readdirSync(episodePath)
57
+ .filter((file) => extname(file) === '.md')
58
+ .map((file) => {
59
+ try {
60
+ const filePath = join(episodePath, file);
61
+ const content = require('node:fs').readFileSync(filePath, 'utf-8');
62
+ const { metadata, content: markdownContent } = parseMarkdown(content);
63
+ const stats = getTextStats(markdownContent);
64
+ return {
65
+ file: filePath,
66
+ metadata,
67
+ stats,
68
+ };
69
+ }
70
+ catch (_error) {
71
+ errors++;
72
+ return null;
73
+ }
74
+ })
75
+ .filter((ch) => ch !== null);
76
+ for (const chapterData of chapters) {
77
+ if (!chapterData)
78
+ continue;
79
+ const chNumber = chapterData.metadata.chapter;
80
+ if (!chNumber)
81
+ continue;
82
+ try {
83
+ const existing = await tracker.getChapter(timeline, arcName, ep.number, chNumber);
84
+ const data = {
85
+ timelineName: timeline,
86
+ arcName: arcName,
87
+ episodeNumber: ep.number,
88
+ partNumber: chapterData.metadata.part || 1,
89
+ number: chNumber,
90
+ pov: chapterData.metadata.pov || 'Unknown',
91
+ title: chapterData.metadata.title || 'Untitled',
92
+ date: new Date(chapterData.metadata.date || Date.now()),
93
+ excerpt: chapterData.metadata.excerpt || '',
94
+ location: chapterData.metadata.location || '',
95
+ outfit: chapterData.metadata.outfit || '',
96
+ kink: chapterData.metadata.kink || '',
97
+ words: chapterData.stats.words,
98
+ characters: chapterData.stats.characters,
99
+ charactersNoSpaces: chapterData.stats.charactersNoSpaces,
100
+ paragraphs: chapterData.stats.paragraphs,
101
+ sentences: chapterData.stats.sentences,
102
+ readingTimeMinutes: Math.ceil(chapterData.stats.words / 200),
103
+ };
104
+ if (existing) {
105
+ await tracker.updateChapter(timeline, arcName, ep.number, chNumber, data);
106
+ updated++;
107
+ }
108
+ else {
109
+ await tracker.createChapter(data);
110
+ added++;
111
+ }
112
+ }
113
+ catch (_error) {
114
+ errors++;
115
+ }
116
+ }
117
+ }
118
+ }
119
+ try {
120
+ const dbArcs = await tracker.getArcs(timeline);
121
+ for (const arc of dbArcs) {
122
+ const dbEpisodes = await tracker.getEpisodes(timeline, arc.name);
123
+ for (const episode of dbEpisodes) {
124
+ const allChapters = await tracker.getChapters(timeline, arc.name, episode.number);
125
+ for (const dbChapter of allChapters) {
126
+ let fileExists = false;
127
+ try {
128
+ const arcPath = join(args.contentPath, dbChapter.arcName);
129
+ if (existsSync(arcPath)) {
130
+ const episodeDirs = readdirSync(arcPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() &&
131
+ entry.name.startsWith(`ep${dbChapter.episodeNumber.toString().padStart(2, '0')}-`));
132
+ for (const episodeDir of episodeDirs) {
133
+ const episodePath = join(arcPath, episodeDir.name);
134
+ const chapterFiles = readdirSync(episodePath).filter((file) => file.includes(`ch${dbChapter.number.toString().padStart(3, '0')}`) &&
135
+ file.endsWith('.md'));
136
+ if (chapterFiles.length > 0) {
137
+ fileExists = true;
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ catch (_error) {
144
+ fileExists = false;
145
+ }
146
+ if (!fileExists) {
147
+ try {
148
+ await tracker.deleteChapter(dbChapter.timelineName, dbChapter.arcName, dbChapter.episodeNumber, dbChapter.number);
149
+ deleted++;
150
+ }
151
+ catch (_error) {
152
+ errors++;
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ catch (_error) {
160
+ errors++;
161
+ }
162
+ return {
163
+ content: [
164
+ {
165
+ type: 'text',
166
+ text: JSON.stringify({
167
+ timeline,
168
+ contentPath: args.contentPath,
169
+ summary: {
170
+ added,
171
+ updated,
172
+ deleted,
173
+ errors,
174
+ },
175
+ }, null, 2),
176
+ },
177
+ ],
178
+ };
179
+ }
180
+ catch (error) {
181
+ throw new Error(`Failed to sync timeline: ${error instanceof Error ? error.message : 'Unknown error'}`);
182
+ }
183
+ }
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ export declare const wordsCountSchema: z.ZodObject<{
3
+ file: z.ZodString;
4
+ }, "strip", z.ZodTypeAny, {
5
+ file: string;
6
+ }, {
7
+ file: string;
8
+ }>;
9
+ export declare function wordsCount(args: z.infer<typeof wordsCountSchema>): Promise<{
10
+ content: {
11
+ type: "text";
12
+ text: string;
13
+ }[];
14
+ }>;
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { getTextStats } from '@echoes-io/utils';
3
+ import { z } from 'zod';
4
+ export const wordsCountSchema = z.object({
5
+ file: z.string().describe('Path to markdown file'),
6
+ });
7
+ export async function wordsCount(args) {
8
+ try {
9
+ const content = readFileSync(args.file, 'utf-8');
10
+ const stats = getTextStats(content);
11
+ return {
12
+ content: [
13
+ {
14
+ type: 'text',
15
+ text: JSON.stringify({
16
+ file: args.file,
17
+ words: stats.words,
18
+ characters: stats.characters,
19
+ charactersNoSpaces: stats.charactersNoSpaces,
20
+ paragraphs: stats.paragraphs,
21
+ sentences: stats.sentences,
22
+ }, null, 2),
23
+ },
24
+ ],
25
+ };
26
+ }
27
+ catch (error) {
28
+ throw new Error(`Failed to count words: ${error instanceof Error ? error.message : 'Unknown error'}`);
29
+ }
30
+ }
package/lib/utils.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function getTimeline(): string;
package/lib/utils.js ADDED
@@ -0,0 +1,7 @@
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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@echoes-io/mcp-server",
3
3
  "type": "module",
4
- "version": "1.0.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
  }