Skip to content

Commit 94cb9bc

Browse files
committed
feat: add create_discovery_log tool to mcp server and update lint config
1 parent 5a7c291 commit 94cb9bc

File tree

2 files changed

+142
-13
lines changed

2 files changed

+142
-13
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"build": "craco build",
5656
"test": "craco test",
5757
"eject": "react-scripts eject",
58-
"lint": "eslint \"src/**/*.{js,jsx}\" \"scripts/**/*.js\" --fix",
58+
"lint": "eslint \"src/**/*.{js,jsx}\" \"scripts/**/*.{js,mjs}\" --fix",
5959
"format": "prettier --write \"src/**/*.{js,jsx,css,json}\"",
6060
"predeploy": "npm run build",
6161
"deploy": "gh-pages -d build -b gh-pages",

scripts/mcp-server/index.mjs

Lines changed: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,15 @@ import {
99
import fs from 'fs/promises';
1010
import path from 'path';
1111
import { fileURLToPath } from 'url';
12-
import { exec } from 'child_process';
13-
import util from 'util';
14-
15-
const execPromise = util.promisify(exec);
12+
import piml from 'piml';
1613

1714
const __filename = fileURLToPath(import.meta.url);
1815
const __dirname = path.dirname(__filename);
1916

2017
// Constants
2118
const POSTS_DIR = path.resolve(__dirname, '../../public/posts');
2219
const POSTS_JSON = path.join(POSTS_DIR, 'posts.json');
23-
const PROJECT_ROOT = path.resolve(__dirname, '../../');
20+
const LOGS_DIR = path.resolve(__dirname, '../../public/logs');
2421

2522
// Helper functions
2623
async function readPosts() {
@@ -83,17 +80,71 @@ async function createPost(args) {
8380
posts.unshift(newPost); // Add to beginning
8481
await writePosts(posts);
8582

86-
// Generate RSS and Sitemap
83+
return `Blog post "${title}" created successfully at ${filePath}.`;
84+
}
85+
86+
async function createDiscoveryLog(args) {
87+
const { type, title, slug, rating, description, content, date, ...otherFields } = args;
88+
89+
// Validate slug
90+
if (!/^[a-z0-9-]+$/.test(slug)) {
91+
throw new Error('Slug must contain only lowercase letters, numbers, and hyphens.');
92+
}
93+
94+
const typeLower = type.toLowerCase();
95+
const categoryDir = path.join(LOGS_DIR, typeLower);
96+
const pimlPath = path.join(categoryDir, `${typeLower}.piml`);
97+
98+
// Ensure directory exists
99+
await fs.mkdir(categoryDir, { recursive: true });
100+
101+
// Read existing PIML
102+
let logs = [];
87103
try {
88-
await execPromise('npm run generate-rss', { cwd: PROJECT_ROOT });
89-
await execPromise('npm run generate-sitemap', { cwd: PROJECT_ROOT });
104+
const pimlString = await fs.readFile(pimlPath, 'utf-8');
105+
const parsed = piml.parse(pimlString);
106+
logs = parsed.logs || [];
90107
} catch (error) {
91-
console.error('Error generating RSS/Sitemap:', error);
92-
// Don't fail the whole operation if generation fails, but log it
93-
return `Blog post "${title}" created at ${filePath}, but RSS/Sitemap generation failed: ${error.message}`;
108+
if (error.code !== 'ENOENT') {
109+
throw error;
110+
}
111+
}
112+
113+
// Check if log exists
114+
if (logs.some(item => item.slug === slug)) {
115+
throw new Error(`Log with slug "${slug}" already exists in ${typeLower}.`);
116+
}
117+
118+
// Create new item
119+
const today = new Date();
120+
const dateStr = date || today.getFullYear() + '-' +
121+
String(today.getMonth() + 1).padStart(2, '0') + '-' +
122+
String(today.getDate()).padStart(2, '0');
123+
124+
const newItem = {
125+
category: type.charAt(0).toUpperCase() + type.slice(1),
126+
date: dateStr,
127+
rating: parseInt(rating, 10),
128+
slug,
129+
title,
130+
description,
131+
...otherFields
132+
};
133+
134+
// Add to beginning
135+
logs.unshift(newItem);
136+
137+
// Write back PIML
138+
const newPimlString = piml.stringify({ logs });
139+
await fs.writeFile(pimlPath, newPimlString, 'utf-8');
140+
141+
// Create detailed content if provided
142+
if (content) {
143+
const txtPath = path.join(categoryDir, `${slug}.txt`);
144+
await fs.writeFile(txtPath, content, 'utf-8');
94145
}
95146

96-
return `Blog post "${title}" created successfully at ${filePath}. RSS and Sitemap updated.`;
147+
return `Discovery log "${title}" created successfully in ${typeLower}.`;
97148
}
98149

99150
// Server setup
@@ -153,6 +204,60 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
153204
required: ["title", "slug", "description", "content"],
154205
},
155206
},
207+
{
208+
name: "create_discovery_log",
209+
description: "Add a new log to discovery logs. This updates the .piml file and creates an optional .txt file.",
210+
inputSchema: {
211+
type: "object",
212+
properties: {
213+
type: {
214+
type: "string",
215+
description: "Log category (e.g., 'book', 'movie', 'video', 'game', 'article', 'music', 'series', 'food', 'websites', 'tools', 'event')",
216+
enum: ["book", "movie", "video", "game", "article", "music", "series", "food", "websites", "tools", "event"]
217+
},
218+
title: {
219+
type: "string",
220+
description: "The title of the log entry",
221+
},
222+
slug: {
223+
type: "string",
224+
description: "The URL-friendly slug for the log (lowercase, hyphens only)",
225+
},
226+
date: {
227+
type: "string",
228+
description: "Date of the discovery (YYYY-MM-DD). Defaults to today.",
229+
},
230+
rating: {
231+
type: "number",
232+
description: "Rating from 1 to 5",
233+
minimum: 1,
234+
maximum: 5
235+
},
236+
description: {
237+
type: "string",
238+
description: "A short description/summary of the discovery",
239+
},
240+
content: {
241+
type: "string",
242+
description: "Optional detailed thoughts (will be saved in a .txt file)",
243+
},
244+
link: {
245+
type: "string",
246+
description: "Optional URL link to the item",
247+
},
248+
image: {
249+
type: "string",
250+
description: "Optional URL/path to a cover image",
251+
},
252+
artist: { type: "string", description: "Artist name (for music)" },
253+
author: { type: "string", description: "Author name (for books/articles)" },
254+
director: { type: "string", description: "Director name (for movies/series)" },
255+
platform: { type: "string", description: "Platform name (for games/tools)" },
256+
album: { type: "string", description: "Album name (for music)" },
257+
},
258+
required: ["type", "title", "slug", "rating", "description"],
259+
},
260+
},
156261
],
157262
};
158263
});
@@ -182,6 +287,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
182287
};
183288
}
184289
}
290+
291+
if (request.params.name === "create_discovery_log") {
292+
try {
293+
const result = await createDiscoveryLog(request.params.arguments);
294+
return {
295+
content: [
296+
{
297+
type: "text",
298+
text: result,
299+
},
300+
],
301+
};
302+
} catch (error) {
303+
return {
304+
content: [
305+
{
306+
type: "text",
307+
text: `Error creating discovery log: ${error.message}`,
308+
},
309+
],
310+
isError: true,
311+
};
312+
}
313+
}
185314
throw new Error("Tool not found");
186315
});
187316

0 commit comments

Comments
 (0)