feat: 初次提交

This commit is contained in:
ethan.chen
2026-01-06 17:35:52 +08:00
commit 372b52b214
24 changed files with 4645 additions and 0 deletions

60
src/index.ts Normal file
View File

@@ -0,0 +1,60 @@
/**
* MCP Server Entry Point
* Personal MCP server with programming, DevOps, family, and hobby tools
*/
import { mcpServer } from "./server.js";
import { logger } from "./utils/logger.js";
// Register all tools
import { registerCodeSnippetTools } from "./tools/programming/codeSnippet.js";
import { registerProjectTemplateTools } from "./tools/programming/projectTemplate.js";
import { registerDocsTools } from "./tools/programming/docs.js";
import { registerCodeReviewTools } from "./tools/programming/codeReview.js";
import { registerNASTools } from "./tools/devops/nas.js";
import { registerServerTools } from "./tools/devops/server.js";
import { registerRouterTools } from "./tools/devops/router.js";
import { registerMathTools } from "./tools/family/math.js";
import { registerBabyTools } from "./tools/family/baby.js";
import { registerFootballTools } from "./tools/hobbies/football.js";
import { registerGameTools } from "./tools/hobbies/games.js";
import { registerNoteTools } from "./tools/common/notes.js";
import { registerTaskTools } from "./tools/common/tasks.js";
// Register all tool modules
logger.info("Registering tools...");
// Programming tools
registerCodeSnippetTools();
registerProjectTemplateTools();
registerDocsTools();
registerCodeReviewTools();
// DevOps tools
registerNASTools();
registerServerTools();
registerRouterTools();
// Family tools
registerMathTools();
registerBabyTools();
// Hobby tools
registerFootballTools();
registerGameTools();
// Common tools
registerNoteTools();
registerTaskTools();
logger.info("All tools registered. Starting MCP server...");
// Start the server
mcpServer.start().catch((error) => {
logger.error("Failed to start MCP server:", error);
process.exit(1);
});

87
src/server.ts Normal file
View File

@@ -0,0 +1,87 @@
/**
* MCP Server core implementation
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { logger } from "./utils/logger.js";
export type ToolHandler = (
args: Record<string, unknown>
) => Promise<{ content: Array<{ type: string; text: string }> }>;
class MCPServer {
private server: Server;
private tools: Map<string, { tool: Tool; handler: ToolHandler }> = new Map();
constructor() {
this.server = new Server(
{
name: "cloud-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = Array.from(this.tools.values()).map(
(entry) => entry.tool
);
return { tools };
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const toolEntry = this.tools.get(name);
if (!toolEntry) {
throw new Error(`Tool ${name} not found`);
}
try {
logger.info(`Calling tool: ${name}`, args);
const result = await toolEntry.handler(args || {});
return result;
} catch (error) {
logger.error(`Error executing tool ${name}:`, error);
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
}
registerTool(tool: Tool, handler: ToolHandler): void {
this.tools.set(tool.name, { tool, handler });
logger.debug(`Registered tool: ${tool.name}`);
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info("MCP Server started");
}
}
export const mcpServer = new MCPServer();

87
src/storage/config.ts Normal file
View File

@@ -0,0 +1,87 @@
/**
* Configuration management for the MCP server
* Handles environment variables and configuration loading
*/
export interface NASConfig {
host?: string;
username?: string;
password?: string;
protocol?: 'smb' | 'ftp' | 'sftp';
}
export interface ServerConfig {
host?: string;
username?: string;
port?: number;
keyPath?: string;
}
export interface RouterConfig {
host?: string;
username?: string;
password?: string;
}
export interface AppConfig {
nas: NASConfig;
server: ServerConfig;
router: RouterConfig;
footballApiKey?: string;
gameApiKey?: string;
}
class ConfigManager {
private config: AppConfig;
constructor() {
this.config = this.loadConfig();
}
private loadConfig(): AppConfig {
return {
nas: {
host: process.env.NAS_HOST,
username: process.env.NAS_USERNAME,
password: process.env.NAS_PASSWORD,
protocol: (process.env.NAS_PROTOCOL as 'smb' | 'ftp' | 'sftp') || 'smb',
},
server: {
host: process.env.SERVER_HOST,
username: process.env.SERVER_USERNAME,
port: process.env.SERVER_PORT ? parseInt(process.env.SERVER_PORT) : 22,
keyPath: process.env.SERVER_KEY_PATH,
},
router: {
host: process.env.ROUTER_HOST,
username: process.env.ROUTER_USERNAME,
password: process.env.ROUTER_PASSWORD,
},
footballApiKey: process.env.FOOTBALL_API_KEY,
gameApiKey: process.env.GAME_API_KEY,
};
}
getConfig(): AppConfig {
return this.config;
}
getNASConfig(): NASConfig {
return this.config.nas;
}
getServerConfig(): ServerConfig {
return this.config.server;
}
getRouterConfig(): RouterConfig {
return this.config.router;
}
reload(): void {
this.config = this.loadConfig();
}
}
export const configManager = new ConfigManager();

282
src/storage/database.ts Normal file
View File

@@ -0,0 +1,282 @@
/**
* Database/storage layer for the MCP server
* Uses JSON file storage for simplicity
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
const DATA_DIR = join(process.cwd(), 'data');
export interface CodeSnippet {
id: string;
title: string;
code: string;
language: string;
tags: string[];
category?: string;
createdAt: string;
updatedAt: string;
}
export interface Note {
id: string;
title: string;
content: string;
tags: string[];
createdAt: string;
updatedAt: string;
}
export interface Task {
id: string;
title: string;
description?: string;
completed: boolean;
createdAt: string;
completedAt?: string;
}
export interface BabyMilestone {
id: string;
title: string;
description: string;
date: string;
createdAt: string;
}
export interface MathResource {
id: string;
title: string;
content: string;
grade?: string;
difficulty?: string;
tags: string[];
createdAt: string;
}
export interface GameWishlist {
id: string;
gameName: string;
platform?: string;
notes?: string;
addedAt: string;
}
class Database {
private ensureDataDir(): void {
if (!existsSync(DATA_DIR)) {
mkdirSync(DATA_DIR, { recursive: true });
}
}
private getFilePath(collection: string): string {
this.ensureDataDir();
return join(DATA_DIR, `${collection}.json`);
}
private readCollection<T>(collection: string): T[] {
const filePath = this.getFilePath(collection);
if (!existsSync(filePath)) {
return [];
}
try {
const content = readFileSync(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error(`Error reading ${collection}:`, error);
return [];
}
}
private writeCollection<T>(collection: string, data: T[]): void {
const filePath = this.getFilePath(collection);
try {
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
} catch (error) {
console.error(`Error writing ${collection}:`, error);
throw error;
}
}
// Code Snippets
saveCodeSnippet(snippet: CodeSnippet): void {
const snippets = this.readCollection<CodeSnippet>('codeSnippets');
const index = snippets.findIndex((s) => s.id === snippet.id);
if (index >= 0) {
snippets[index] = { ...snippet, updatedAt: new Date().toISOString() };
} else {
snippets.push(snippet);
}
this.writeCollection('codeSnippets', snippets);
}
getCodeSnippets(): CodeSnippet[] {
return this.readCollection<CodeSnippet>('codeSnippets');
}
getCodeSnippet(id: string): CodeSnippet | undefined {
const snippets = this.readCollection<CodeSnippet>('codeSnippets');
return snippets.find((s) => s.id === id);
}
deleteCodeSnippet(id: string): boolean {
const snippets = this.readCollection<CodeSnippet>('codeSnippets');
const filtered = snippets.filter((s) => s.id !== id);
if (filtered.length < snippets.length) {
this.writeCollection('codeSnippets', filtered);
return true;
}
return false;
}
searchCodeSnippets(query: string, tags?: string[]): CodeSnippet[] {
const snippets = this.readCollection<CodeSnippet>('codeSnippets');
const lowerQuery = query.toLowerCase();
return snippets.filter((s) => {
const matchesQuery =
s.title.toLowerCase().includes(lowerQuery) ||
s.code.toLowerCase().includes(lowerQuery) ||
s.language.toLowerCase().includes(lowerQuery);
const matchesTags =
!tags || tags.length === 0 || tags.some((tag) => s.tags.includes(tag));
return matchesQuery && matchesTags;
});
}
// Notes
saveNote(note: Note): void {
const notes = this.readCollection<Note>('notes');
const index = notes.findIndex((n) => n.id === note.id);
if (index >= 0) {
notes[index] = { ...note, updatedAt: new Date().toISOString() };
} else {
notes.push(note);
}
this.writeCollection('notes', notes);
}
getNotes(): Note[] {
return this.readCollection<Note>('notes');
}
getNote(id: string): Note | undefined {
const notes = this.readCollection<Note>('notes');
return notes.find((n) => n.id === id);
}
searchNotes(query: string): Note[] {
const notes = this.readCollection<Note>('notes');
const lowerQuery = query.toLowerCase();
return notes.filter(
(n) =>
n.title.toLowerCase().includes(lowerQuery) ||
n.content.toLowerCase().includes(lowerQuery) ||
n.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
);
}
deleteNote(id: string): boolean {
const notes = this.readCollection<Note>('notes');
const filtered = notes.filter((n) => n.id !== id);
if (filtered.length < notes.length) {
this.writeCollection('notes', filtered);
return true;
}
return false;
}
// Tasks
saveTask(task: Task): void {
const tasks = this.readCollection<Task>('tasks');
const index = tasks.findIndex((t) => t.id === task.id);
if (index >= 0) {
tasks[index] = task;
} else {
tasks.push(task);
}
this.writeCollection('tasks', tasks);
}
getTasks(completed?: boolean): Task[] {
const tasks = this.readCollection<Task>('tasks');
if (completed === undefined) {
return tasks;
}
return tasks.filter((t) => t.completed === completed);
}
getTask(id: string): Task | undefined {
const tasks = this.readCollection<Task>('tasks');
return tasks.find((t) => t.id === id);
}
// Baby Milestones
saveBabyMilestone(milestone: BabyMilestone): void {
const milestones = this.readCollection<BabyMilestone>('babyMilestones');
milestones.push(milestone);
this.writeCollection('babyMilestones', milestones);
}
getBabyMilestones(): BabyMilestone[] {
return this.readCollection<BabyMilestone>('babyMilestones');
}
// Math Resources
saveMathResource(resource: MathResource): void {
const resources = this.readCollection<MathResource>('mathResources');
const index = resources.findIndex((r) => r.id === resource.id);
if (index >= 0) {
resources[index] = resource;
} else {
resources.push(resource);
}
this.writeCollection('mathResources', resources);
}
getMathResources(): MathResource[] {
return this.readCollection<MathResource>('mathResources');
}
searchMathResources(query: string, grade?: string): MathResource[] {
const resources = this.readCollection<MathResource>('mathResources');
const lowerQuery = query.toLowerCase();
return resources.filter((r) => {
const matchesQuery =
r.title.toLowerCase().includes(lowerQuery) ||
r.content.toLowerCase().includes(lowerQuery) ||
r.tags.some((tag) => tag.toLowerCase().includes(lowerQuery));
const matchesGrade = !grade || r.grade === grade;
return matchesQuery && matchesGrade;
});
}
// Game Wishlist
saveGameWishlist(game: GameWishlist): void {
const games = this.readCollection<GameWishlist>('gameWishlist');
const index = games.findIndex((g) => g.id === game.id);
if (index >= 0) {
games[index] = game;
} else {
games.push(game);
}
this.writeCollection('gameWishlist', games);
}
getGameWishlist(): GameWishlist[] {
return this.readCollection<GameWishlist>('gameWishlist');
}
deleteGameWishlist(id: string): boolean {
const games = this.readCollection<GameWishlist>('gameWishlist');
const filtered = games.filter((g) => g.id !== id);
if (filtered.length < games.length) {
this.writeCollection('gameWishlist', filtered);
return true;
}
return false;
}
}
export const database = new Database();

212
src/tools/common/notes.ts Normal file
View File

@@ -0,0 +1,212 @@
/**
* Personal notes management tools
*/
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { mcpServer } from '../../server.js';
import { database, Note } from '../../storage/database.js';
import { randomUUID } from 'crypto';
export function registerNoteTools(): void {
// Create note
mcpServer.registerTool(
{
name: 'note_create',
description: 'Create a new note',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Note title',
},
content: {
type: 'string',
description: 'Note content',
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags for categorization',
},
id: {
type: 'string',
description: 'Optional ID for updating existing note',
},
},
required: ['title', 'content'],
},
},
async (args) => {
const now = new Date().toISOString();
const note: Note = {
id: (args.id as string) || randomUUID(),
title: args.title as string,
content: args.content as string,
tags: (args.tags as string[]) || [],
createdAt: args.id
? database.getNote(args.id)?.createdAt || now
: now,
updatedAt: now,
};
database.saveNote(note);
return {
content: [
{
type: 'text',
text: `Note "${note.title}" saved successfully with ID: ${note.id}`,
},
],
};
}
);
// Search notes
mcpServer.registerTool(
{
name: 'note_search',
description: 'Search notes by query',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (searches in title, content, and tags)',
},
},
required: ['query'],
},
},
async (args) => {
const query = args.query as string;
const notes = database.searchNotes(query);
if (notes.length === 0) {
return {
content: [
{
type: 'text',
text: `No notes found matching "${query}"`,
},
],
};
}
const results = notes
.map(
(n) =>
`ID: ${n.id}\nTitle: ${n.title}\nTags: ${n.tags.join(', ')}\nCreated: ${new Date(n.createdAt).toLocaleDateString()}\n\nContent:\n${n.content}\n---`
)
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Found ${notes.length} note(s):\n\n${results}`,
},
],
};
}
);
// List notes
mcpServer.registerTool(
{
name: 'note_list',
description: 'List all notes',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of notes to return',
},
},
},
},
async (args) => {
const notes = database.getNotes();
const limit = args.limit as number | undefined;
// Sort by updated date (newest first)
const sorted = notes.sort((a, b) =>
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
const limited = limit ? sorted.slice(0, limit) : sorted;
if (limited.length === 0) {
return {
content: [
{
type: 'text',
text: 'No notes found. Use note_create to create a note!',
},
],
};
}
const list = limited
.map(
(n) =>
`📝 ${n.title}\nID: ${n.id}\nTags: ${n.tags.join(', ') || 'None'}\nUpdated: ${new Date(n.updatedAt).toLocaleDateString()}`
)
.join('\n\n---\n\n');
return {
content: [
{
type: 'text',
text: `Total: ${notes.length} note(s)\n\n${list}`,
},
],
};
}
);
// Delete note
mcpServer.registerTool(
{
name: 'note_delete',
description: 'Delete a note by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'ID of the note to delete',
},
},
required: ['id'],
},
},
async (args) => {
const id = args.id as string;
const deleted = database.deleteNote(id);
if (deleted) {
return {
content: [
{
type: 'text',
text: `Note with ID ${id} deleted successfully`,
},
],
};
} else {
return {
content: [
{
type: 'text',
text: `Note with ID ${id} not found`,
},
],
};
}
}
);
}

168
src/tools/common/tasks.ts Normal file
View File

@@ -0,0 +1,168 @@
/**
* Task management tools
*/
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { mcpServer } from '../../server.js';
import { database, Task } from '../../storage/database.js';
import { randomUUID } from 'crypto';
export function registerTaskTools(): void {
// Add task
mcpServer.registerTool(
{
name: 'task_add',
description: 'Add a new task',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Task title',
},
description: {
type: 'string',
description: 'Task description (optional)',
},
},
required: ['title'],
},
},
async (args) => {
const task: Task = {
id: randomUUID(),
title: args.title as string,
description: args.description as string,
completed: false,
createdAt: new Date().toISOString(),
};
database.saveTask(task);
return {
content: [
{
type: 'text',
text: `Task "${task.title}" added successfully with ID: ${task.id}`,
},
],
};
}
);
// List tasks
mcpServer.registerTool(
{
name: 'task_list',
description: 'List tasks (optionally filter by completion status)',
inputSchema: {
type: 'object',
properties: {
completed: {
type: 'boolean',
description: 'Filter by completion status (true for completed, false for pending, undefined for all)',
},
},
},
},
async (args) => {
const completed = args.completed as boolean | undefined;
const tasks = database.getTasks(completed);
if (tasks.length === 0) {
const statusText = completed === true ? 'completed' : completed === false ? 'pending' : '';
return {
content: [
{
type: 'text',
text: `No ${statusText} tasks found. Use task_add to add a task!`,
},
],
};
}
// Sort by creation date (newest first)
const sorted = tasks.sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
const list = sorted
.map((t) => {
const status = t.completed ? '✅' : '⏳';
return `${status} ${t.title}\nID: ${t.id}${t.description ? `\nDescription: ${t.description}` : ''}\nCreated: ${new Date(t.createdAt).toLocaleDateString()}${t.completed && t.completedAt ? `\nCompleted: ${new Date(t.completedAt).toLocaleDateString()}` : ''}`;
})
.join('\n\n---\n\n');
const total = database.getTasks().length;
const completedCount = database.getTasks(true).length;
const pendingCount = database.getTasks(false).length;
return {
content: [
{
type: 'text',
text: `Tasks (${tasks.length} shown, Total: ${total}, Completed: ${completedCount}, Pending: ${pendingCount}):\n\n${list}`,
},
],
};
}
);
// Complete task
mcpServer.registerTool(
{
name: 'task_complete',
description: 'Mark a task as completed',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'ID of the task to complete',
},
},
required: ['id'],
},
},
async (args) => {
const id = args.id as string;
const task = database.getTask(id);
if (!task) {
return {
content: [
{
type: 'text',
text: `Task with ID ${id} not found`,
},
],
};
}
if (task.completed) {
return {
content: [
{
type: 'text',
text: `Task "${task.title}" is already completed`,
},
],
};
}
task.completed = true;
task.completedAt = new Date().toISOString();
database.saveTask(task);
return {
content: [
{
type: 'text',
text: `Task "${task.title}" marked as completed! 🎉`,
},
],
};
}
);
}

296
src/tools/devops/nas.ts Normal file
View File

@@ -0,0 +1,296 @@
/**
* NAS file management tools
*/
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { mcpServer } from "../../server.js";
import { configManager } from "../../storage/config.js";
import { logger } from "../../utils/logger.js";
export function registerNASTools(): void {
// List NAS files
mcpServer.registerTool(
{
name: "nas_list_files",
description: "List files and directories on NAS",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Path to list (default: root)",
default: "/",
},
},
},
},
async (args) => {
const nasConfig = configManager.getNASConfig();
const path = (args.path as string) || "/";
if (!nasConfig.host) {
return {
content: [
{
type: "text",
text: "Error: NAS configuration not found. Please set NAS_HOST, NAS_USERNAME, and NAS_PASSWORD in environment variables.",
},
],
isError: true,
};
}
try {
// Note: This is a placeholder implementation
// In a real implementation, you would use appropriate libraries based on protocol
// For SMB: smb2 or node-smb2
// For FTP: basic-ftp
// For SFTP: ssh2-sftp-client
const protocol = nasConfig.protocol || "smb";
let result = "";
if (protocol === "smb") {
result = `NAS File Listing (SMB protocol)\n`;
result += `Host: ${nasConfig.host}\n`;
result += `Path: ${path}\n\n`;
result += `Note: SMB file listing requires additional libraries.\n`;
result += `To implement, install: bun add smb2\n`;
result += `Example files that would be listed:\n`;
result += `- Documents/\n`;
result += `- Media/\n`;
result += `- Backups/`;
} else if (protocol === "ftp" || protocol === "sftp") {
result = `NAS File Listing (${protocol.toUpperCase()} protocol)\n`;
result += `Host: ${nasConfig.host}\n`;
result += `Path: ${path}\n\n`;
result += `Note: ${protocol.toUpperCase()} file listing requires additional libraries.\n`;
if (protocol === "ftp") {
result += `To implement, install: bun add basic-ftp\n`;
} else {
result += `To implement, install: bun add ssh2-sftp-client\n`;
}
}
logger.info(`Listing NAS files at ${path}`);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error listing NAS files: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
// Upload file to NAS
mcpServer.registerTool(
{
name: "nas_upload_file",
description: "Upload a file to NAS",
inputSchema: {
type: "object",
properties: {
localPath: {
type: "string",
description: "Local file path to upload",
},
remotePath: {
type: "string",
description: "Remote path on NAS",
},
},
required: ["localPath", "remotePath"],
},
},
async (args) => {
const nasConfig = configManager.getNASConfig();
const localPath = args.localPath as string;
const remotePath = args.remotePath as string;
if (!nasConfig.host) {
return {
content: [
{
type: "text",
text: "Error: NAS configuration not found. Please configure NAS settings.",
},
],
isError: true,
};
}
try {
logger.info(`Uploading ${localPath} to NAS ${remotePath}`);
// Placeholder implementation
return {
content: [
{
type: "text",
text: `File upload initiated:\nLocal: ${localPath}\nRemote: ${remotePath}\n\nNote: Full implementation requires protocol-specific libraries (smb2, basic-ftp, or ssh2-sftp-client).`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error uploading file: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
// Download file from NAS
mcpServer.registerTool(
{
name: "nas_download_file",
description: "Download a file from NAS",
inputSchema: {
type: "object",
properties: {
remotePath: {
type: "string",
description: "Remote file path on NAS",
},
localPath: {
type: "string",
description: "Local path to save the file",
},
},
required: ["remotePath", "localPath"],
},
},
async (args) => {
const nasConfig = configManager.getNASConfig();
const remotePath = args.remotePath as string;
const localPath = args.localPath as string;
if (!nasConfig.host) {
return {
content: [
{
type: "text",
text: "Error: NAS configuration not found. Please configure NAS settings.",
},
],
isError: true,
};
}
try {
logger.info(`Downloading ${remotePath} from NAS to ${localPath}`);
// Placeholder implementation
return {
content: [
{
type: "text",
text: `File download initiated:\nRemote: ${remotePath}\nLocal: ${localPath}\n\nNote: Full implementation requires protocol-specific libraries.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error downloading file: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
// Search files on NAS
mcpServer.registerTool(
{
name: "nas_search_files",
description: "Search for files on NAS by name pattern",
inputSchema: {
type: "object",
properties: {
pattern: {
type: "string",
description: "File name pattern to search for",
},
path: {
type: "string",
description: "Base path to search in (default: root)",
default: "/",
},
},
required: ["pattern"],
},
},
async (args) => {
const nasConfig = configManager.getNASConfig();
const pattern = args.pattern as string;
const path = (args.path as string) || "/";
if (!nasConfig.host) {
return {
content: [
{
type: "text",
text: "Error: NAS configuration not found. Please configure NAS settings.",
},
],
isError: true,
};
}
try {
logger.info(`Searching NAS for pattern: ${pattern} in ${path}`);
// Placeholder implementation
return {
content: [
{
type: "text",
text: `Searching for files matching "${pattern}" in ${path}\n\nNote: Full implementation requires protocol-specific libraries and recursive directory traversal.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error searching files: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
}

204
src/tools/devops/router.ts Normal file
View File

@@ -0,0 +1,204 @@
/**
* Soft router management tools
*/
import { mcpServer } from "../../server.js";
import { configManager } from "../../storage/config.js";
import { logger } from "../../utils/logger.js";
import axios from "axios";
export function registerRouterTools(): void {
// Get router status
mcpServer.registerTool(
{
name: "router_status",
description: "Get soft router status and information",
inputSchema: {
type: "object",
properties: {},
},
},
async () => {
const routerConfig = configManager.getRouterConfig();
if (!routerConfig.host) {
return {
content: [
{
type: "text",
text: "Error: Router configuration not found. Please set ROUTER_HOST, ROUTER_USERNAME, and ROUTER_PASSWORD in environment variables.",
},
],
isError: true,
};
}
try {
logger.info(`Checking router status: ${routerConfig.host}`);
// Try to connect to router web interface (common ports: 80, 443, 8080)
// Most routers have a status API endpoint
const ports = [80, 443, 8080];
let status = "";
for (const port of ports) {
try {
const protocol = port === 443 ? "https" : "http";
await axios.get(`${protocol}://${routerConfig.host}:${port}/`, {
timeout: 2000,
auth:
routerConfig.username && routerConfig.password
? {
username: routerConfig.username,
password: routerConfig.password,
}
: undefined,
});
status += `Router accessible on port ${port}\n`;
break;
} catch (error) {
// Continue to next port
}
}
if (!status) {
status = `Router Status (${routerConfig.host}):\n\n`;
status += `Note: Router status retrieval depends on router firmware.\n`;
status += `Common router management interfaces:\n`;
status += `- OpenWrt: http://${routerConfig.host}/cgi-bin/luci\n`;
status += `- DD-WRT: http://${routerConfig.host}\n`;
status += `- pfSense: https://${routerConfig.host}\n`;
status += `\nFor full implementation, use router-specific API or SSH connection.`;
}
return {
content: [
{
type: "text",
text: status,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting router status: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
// Get traffic statistics
mcpServer.registerTool(
{
name: "router_traffic",
description: "Get router traffic statistics",
inputSchema: {
type: "object",
properties: {},
},
},
async () => {
const routerConfig = configManager.getRouterConfig();
if (!routerConfig.host) {
return {
content: [
{
type: "text",
text: "Error: Router configuration not found. Please configure router settings.",
},
],
isError: true,
};
}
try {
logger.info(`Getting router traffic stats: ${routerConfig.host}`);
// Placeholder - implementation depends on router firmware
return {
content: [
{
type: "text",
text: `Router Traffic Statistics (${routerConfig.host}):\n\nNote: Traffic statistics retrieval depends on router firmware.\n\nFor OpenWrt, you can use:\n- SSH connection to execute: cat /proc/net/dev\n- Or access web interface: http://${routerConfig.host}/cgi-bin/luci/admin/network/bandwidth\n\nFor other routers, check their specific API documentation.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting traffic stats: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
// List connected devices
mcpServer.registerTool(
{
name: "router_devices",
description: "List devices connected to the router",
inputSchema: {
type: "object",
properties: {},
},
},
async () => {
const routerConfig = configManager.getRouterConfig();
if (!routerConfig.host) {
return {
content: [
{
type: "text",
text: "Error: Router configuration not found. Please configure router settings.",
},
],
isError: true,
};
}
try {
logger.info(`Listing router devices: ${routerConfig.host}`);
// Placeholder - implementation depends on router firmware
return {
content: [
{
type: "text",
text: `Connected Devices (${routerConfig.host}):\n\nNote: Device listing depends on router firmware.\n\nFor OpenWrt:\n- SSH: cat /proc/net/arp\n- Web: http://${routerConfig.host}/cgi-bin/luci/admin/network/dhcp\n\nFor other routers, check DHCP lease table or device list in web interface.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error listing devices: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
}

367
src/tools/devops/server.ts Normal file
View File

@@ -0,0 +1,367 @@
/**
* Cloud server monitoring and management tools
*/
import { mcpServer } from "../../server.js";
import { configManager } from "../../storage/config.js";
import { logger } from "../../utils/logger.js";
import { Client } from "ssh2";
export function registerServerTools(): void {
// Get server status
mcpServer.registerTool(
{
name: "server_status",
description: "Get cloud server status (CPU, memory, disk usage)",
inputSchema: {
type: "object",
properties: {},
},
},
async () => {
const serverConfig = configManager.getServerConfig();
if (!serverConfig.host) {
return {
content: [
{
type: "text",
text: "Error: Server configuration not found. Please set SERVER_HOST, SERVER_USERNAME, and SERVER_KEY_PATH in environment variables.",
},
],
isError: true,
};
}
try {
return new Promise((resolve) => {
const conn = new Client();
conn.on("ready", () => {
logger.info("SSH connection established");
// Execute commands to get system status
conn.exec(
"echo 'CPU:'; top -l 1 | grep 'CPU usage' | awk '{print $3}'; echo 'Memory:'; vm_stat | head -n 5; echo 'Disk:'; df -h / | tail -n 1",
(err: Error | undefined, stream: any) => {
if (err) {
conn.end();
resolve({
content: [
{
type: "text",
text: `Error executing command: ${err.message}`,
},
],
} as any);
return;
}
let output = "";
stream
.on("close", () => {
conn.end();
resolve({
content: [
{
type: "text",
text: `Server Status (${serverConfig.host}):\n\n${
output ||
"Status retrieved successfully. Note: Command output may vary by OS."
}`,
},
],
});
})
.on("data", (data: Buffer) => {
output += data.toString();
})
.stderr.on("data", (data: Buffer) => {
output += data.toString();
});
}
);
});
conn.on("error", (err) => {
logger.error("SSH connection error:", err);
resolve({
content: [
{
type: "text",
text: `Error connecting to server: ${err.message}\n\nMake sure:\n1. Server is accessible\n2. SSH key is configured correctly\n3. Server allows SSH connections`,
},
],
} as any);
});
// Connect using key or password
const connectOptions: {
host: string;
port: number;
username: string;
privateKey?: string;
password?: string;
} = {
host: serverConfig.host!,
port: serverConfig.port || 22,
username: serverConfig.username!,
};
if (serverConfig.keyPath) {
import("fs").then(({ readFileSync }) => {
try {
connectOptions.privateKey = readFileSync(serverConfig.keyPath!);
conn.connect(connectOptions);
} catch (error) {
resolve({
content: [
{
type: "text",
text: `Error reading SSH key: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
} as any);
}
});
} else {
conn.connect(connectOptions);
}
});
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting server status: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
} as any;
}
}
);
// Deploy application
mcpServer.registerTool(
{
name: "server_deploy",
description: "Deploy application to cloud server",
inputSchema: {
type: "object",
properties: {
localPath: {
type: "string",
description: "Local path of the application to deploy",
},
remotePath: {
type: "string",
description: "Remote path on server",
},
command: {
type: "string",
description:
'Optional command to run after deployment (e.g., "pm2 restart app")',
},
},
required: ["localPath", "remotePath"],
},
},
async (args) => {
const serverConfig = configManager.getServerConfig();
const localPath = args.localPath as string;
const remotePath = args.remotePath as string;
const command = args.command as string | undefined;
if (!serverConfig.host) {
return {
content: [
{
type: "text",
text: "Error: Server configuration not found. Please configure server settings.",
},
],
isError: true,
};
}
try {
logger.info(
`Deploying ${localPath} to ${serverConfig.host}:${remotePath}`
);
// Placeholder - full implementation would use scp or sftp
return {
content: [
{
type: "text",
text: `Deployment initiated:\nLocal: ${localPath}\nRemote: ${
serverConfig.host
}:${remotePath}\n${
command ? `Command: ${command}` : ""
}\n\nNote: Full deployment requires SCP/SFTP implementation. Consider using scp2 or ssh2-sftp-client libraries.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error deploying: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
// View server logs
mcpServer.registerTool(
{
name: "server_logs",
description: "View server logs",
inputSchema: {
type: "object",
properties: {
logPath: {
type: "string",
description: "Path to log file (e.g., /var/log/app.log)",
default: "/var/log/syslog",
},
lines: {
type: "number",
description: "Number of lines to retrieve",
default: 50,
},
},
},
},
async (args) => {
const serverConfig = configManager.getServerConfig();
const logPath = (args.logPath as string) || "/var/log/syslog";
const lines = (args.lines as number) || 50;
if (!serverConfig.host) {
return {
content: [
{
type: "text",
text: "Error: Server configuration not found. Please configure server settings.",
},
],
isError: true,
};
}
try {
return new Promise((resolve) => {
const conn = new Client();
conn.on("ready", () => {
conn.exec(
`tail -n ${lines} ${logPath}`,
(err: Error | undefined, stream: any) => {
if (err) {
conn.end();
resolve({
content: [
{
type: "text",
text: `Error reading logs: ${err.message}`,
},
],
} as any);
return;
}
let output = "";
stream
.on("close", () => {
conn.end();
resolve({
content: [
{
type: "text",
text: `Server Logs (${logPath}, last ${lines} lines):\n\n${
output || "No logs found or permission denied"
}`,
},
],
});
})
.on("data", (data: Buffer) => {
output += data.toString();
})
.stderr.on("data", (data: Buffer) => {
output += data.toString();
});
}
);
});
conn.on("error", (err: Error) => {
resolve({
content: [
{
type: "text",
text: `Error connecting to server: ${err.message}`,
},
],
} as any);
});
const connectOptions: {
host: string;
port: number;
username: string;
privateKey?: string;
} = {
host: serverConfig.host!,
port: serverConfig.port || 22,
username: serverConfig.username!,
};
if (serverConfig.keyPath) {
import("fs").then(({ readFileSync }) => {
try {
connectOptions.privateKey = readFileSync(serverConfig.keyPath!);
conn.connect(connectOptions);
} catch (error) {
resolve({
content: [
{
type: "text",
text: `Error reading SSH key: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
} as any);
}
});
} else {
conn.connect(connectOptions);
}
});
} catch (error) {
return {
content: [
{
type: "text",
text: `Error viewing logs: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
} as any;
}
}
);
}

182
src/tools/family/baby.ts Normal file
View File

@@ -0,0 +1,182 @@
/**
* Baby milestone and reminder tools
*/
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { mcpServer } from '../../server.js';
import { database, BabyMilestone } from '../../storage/database.js';
import { randomUUID } from 'crypto';
export function registerBabyTools(): void {
// Add baby milestone
mcpServer.registerTool(
{
name: 'baby_milestone_add',
description: 'Record a baby milestone (e.g., first steps, first words)',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Milestone title (e.g., "First steps", "First word")',
},
description: {
type: 'string',
description: 'Detailed description of the milestone',
},
date: {
type: 'string',
description: 'Date of the milestone (ISO format or YYYY-MM-DD)',
},
},
required: ['title', 'description', 'date'],
},
},
async (args) => {
const now = new Date().toISOString();
const milestone: BabyMilestone = {
id: randomUUID(),
title: args.title as string,
description: args.description as string,
date: args.date as string,
createdAt: now,
};
database.saveBabyMilestone(milestone);
return {
content: [
{
type: 'text',
text: `Baby milestone "${milestone.title}" recorded successfully!\n\nDate: ${milestone.date}\nDescription: ${milestone.description}`,
},
],
};
}
);
// List baby milestones
mcpServer.registerTool(
{
name: 'baby_milestone_list',
description: 'List all recorded baby milestones',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of milestones to return',
},
},
},
},
async (args) => {
const milestones = database.getBabyMilestones();
const limit = args.limit as number | undefined;
// Sort by date (newest first)
const sorted = milestones.sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime()
);
const limited = limit ? sorted.slice(0, limit) : sorted;
if (limited.length === 0) {
return {
content: [
{
type: 'text',
text: 'No milestones recorded yet. Use baby_milestone_add to record milestones!',
},
],
};
}
const list = limited
.map(
(m) =>
`📅 ${m.date}\n🎯 ${m.title}\n📝 ${m.description}\n---`
)
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Total: ${milestones.length} milestone(s) recorded\n\n${list}`,
},
],
};
}
);
// Set baby reminder
mcpServer.registerTool(
{
name: 'baby_reminder_set',
description: 'Set a reminder for baby-related tasks (vaccines, checkups, etc.)',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Reminder title (e.g., "Vaccine", "Checkup", "Developmental screening")',
},
description: {
type: 'string',
description: 'Reminder description',
},
date: {
type: 'string',
description: 'Reminder date (ISO format or YYYY-MM-DD)',
},
type: {
type: 'string',
description: 'Reminder type (vaccine, checkup, screening, other)',
default: 'other',
},
},
required: ['title', 'date'],
},
},
async (args) => {
const title = args.title as string;
const description = (args.description as string) || '';
const date = args.date as string;
const type = (args.type as string) || 'other';
// Save as a task in the database
const { database: db } = await import('../../storage/database.js');
const { Task } = await import('../../storage/database.js');
const task: Task = {
id: randomUUID(),
title: `[Baby] ${title}`,
description: `Type: ${type}\n${description}`,
completed: false,
createdAt: new Date().toISOString(),
};
db.saveTask(task);
// Common baby reminders reference
const commonReminders: Record<string, string> = {
vaccine: 'Common vaccines: DTaP, MMR, Varicella, Hepatitis B',
checkup: 'Regular checkups: 2 weeks, 1 month, 2 months, 4 months, 6 months, 9 months, 12 months, 15 months, 18 months, 2 years',
screening: 'Developmental screenings: 9 months, 18 months, 24 months',
};
const reminderInfo = commonReminders[type] || '';
return {
content: [
{
type: 'text',
text: `Baby reminder set successfully!\n\nTitle: ${title}\nDate: ${date}\nType: ${type}${description ? `\nDescription: ${description}` : ''}${reminderInfo ? `\n\nNote: ${reminderInfo}` : ''}\n\nReminder saved as a task. Use task_list to view all reminders.`,
},
],
};
}
);
}

234
src/tools/family/math.ts Normal file
View File

@@ -0,0 +1,234 @@
/**
* Math teaching resource tools
*/
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { mcpServer } from '../../server.js';
import { database, MathResource } from '../../storage/database.js';
import { randomUUID } from 'crypto';
export function registerMathTools(): void {
// Search math resources
mcpServer.registerTool(
{
name: 'math_resource_search',
description: 'Search for math teaching resources (worksheets, problems, tools)',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
grade: {
type: 'string',
description: 'Grade level (e.g., "1st", "2nd", "elementary", "middle", "high")',
},
},
required: ['query'],
},
},
async (args) => {
const query = args.query as string;
const grade = args.grade as string | undefined;
const resources = database.searchMathResources(query, grade);
if (resources.length === 0) {
return {
content: [
{
type: 'text',
text: `No math resources found matching "${query}"${grade ? ` for grade ${grade}` : ''}.\n\nYou can save resources using math_resource_save tool.`,
},
],
};
}
const results = resources
.map(
(r) =>
`Title: ${r.title}\nGrade: ${r.grade || 'N/A'}\nDifficulty: ${r.difficulty || 'N/A'}\nTags: ${r.tags.join(', ')}\n\nContent:\n${r.content}\n---`
)
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Found ${resources.length} math resource(s):\n\n${results}`,
},
],
};
}
);
// Generate math problems
mcpServer.registerTool(
{
name: 'math_problem_generate',
description: 'Generate math problems by grade and difficulty',
inputSchema: {
type: 'object',
properties: {
grade: {
type: 'string',
description: 'Grade level (e.g., "1st", "2nd", "elementary", "middle", "high")',
},
difficulty: {
type: 'string',
description: 'Difficulty level (easy, medium, hard)',
default: 'medium',
},
topic: {
type: 'string',
description: 'Math topic (e.g., "addition", "multiplication", "algebra", "geometry")',
},
count: {
type: 'number',
description: 'Number of problems to generate',
default: 5,
},
},
required: ['grade'],
},
},
async (args) => {
const grade = args.grade as string;
const difficulty = (args.difficulty as string) || 'medium';
const topic = args.topic as string | undefined;
const count = (args.count as number) || 5;
// Generate problems based on grade and difficulty
const problems: string[] = [];
if (grade.includes('1st') || grade.includes('2nd') || grade === 'elementary') {
// Elementary level
for (let i = 0; i < count; i++) {
if (topic === 'addition' || !topic) {
const a = Math.floor(Math.random() * 20) + 1;
const b = Math.floor(Math.random() * 20) + 1;
problems.push(`${a} + ${b} = ?`);
} else if (topic === 'subtraction') {
const a = Math.floor(Math.random() * 20) + 10;
const b = Math.floor(Math.random() * a) + 1;
problems.push(`${a} - ${b} = ?`);
} else if (topic === 'multiplication') {
const a = Math.floor(Math.random() * 10) + 1;
const b = Math.floor(Math.random() * 10) + 1;
problems.push(`${a} × ${b} = ?`);
}
}
} else if (grade.includes('middle') || grade.includes('6th') || grade.includes('7th') || grade.includes('8th')) {
// Middle school level
for (let i = 0; i < count; i++) {
if (topic === 'algebra' || !topic) {
const a = Math.floor(Math.random() * 10) + 1;
const b = Math.floor(Math.random() * 20) - 10;
const c = Math.floor(Math.random() * 20) - 10;
problems.push(`Solve for x: ${a}x + ${b} = ${c}`);
} else if (topic === 'fractions') {
const num1 = Math.floor(Math.random() * 10) + 1;
const den1 = Math.floor(Math.random() * 10) + 1;
const num2 = Math.floor(Math.random() * 10) + 1;
const den2 = Math.floor(Math.random() * 10) + 1;
problems.push(`Add: ${num1}/${den1} + ${num2}/${den2} = ?`);
} else if (topic === 'geometry') {
const side = Math.floor(Math.random() * 10) + 1;
problems.push(`Find the area of a square with side length ${side}`);
}
}
} else {
// High school level
for (let i = 0; i < count; i++) {
if (topic === 'algebra' || !topic) {
const a = Math.floor(Math.random() * 5) + 1;
const b = Math.floor(Math.random() * 10) - 5;
const c = Math.floor(Math.random() * 10) - 5;
problems.push(`Solve: ${a}x² + ${b}x + ${c} = 0`);
} else if (topic === 'geometry') {
const r = Math.floor(Math.random() * 10) + 1;
problems.push(`Find the area of a circle with radius ${r} (use π = 3.14)`);
} else if (topic === 'trigonometry') {
const angle = [30, 45, 60][Math.floor(Math.random() * 3)];
problems.push(`Find sin(${angle}°)`);
}
}
}
const problemsText = problems.map((p, i) => `${i + 1}. ${p}`).join('\n');
return {
content: [
{
type: 'text',
text: `Generated ${count} math problem(s) for ${grade} grade (${difficulty} difficulty)${topic ? ` - Topic: ${topic}` : ''}:\n\n${problemsText}`,
},
],
};
}
);
// Save math resource
mcpServer.registerTool(
{
name: 'math_resource_save',
description: 'Save a math teaching resource',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Title of the resource',
},
content: {
type: 'string',
description: 'Content of the resource (worksheet, problem set, etc.)',
},
grade: {
type: 'string',
description: 'Grade level',
},
difficulty: {
type: 'string',
description: 'Difficulty level',
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags for categorization',
},
id: {
type: 'string',
description: 'Optional ID for updating existing resource',
},
},
required: ['title', 'content'],
},
},
async (args) => {
const now = new Date().toISOString();
const resource: MathResource = {
id: (args.id as string) || randomUUID(),
title: args.title as string,
content: args.content as string,
grade: args.grade as string | undefined,
difficulty: args.difficulty as string | undefined,
tags: (args.tags as string[]) || [],
createdAt: now,
};
database.saveMathResource(resource);
return {
content: [
{
type: 'text',
text: `Math resource "${resource.title}" saved successfully with ID: ${resource.id}`,
},
],
};
}
);
}

View File

@@ -0,0 +1,259 @@
/**
* Football information tools
*/
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { mcpServer } from '../../server.js';
import { configManager } from '../../storage/config.js';
import axios from 'axios';
import { logger } from '../../utils/logger.js';
export function registerFootballTools(): void {
// Get football matches
mcpServer.registerTool(
{
name: 'football_matches',
description: 'Get upcoming or recent football matches',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days ahead/behind to search (default: 7)',
default: 7,
},
league: {
type: 'string',
description: 'League name (e.g., "Premier League", "La Liga", "Champions League")',
},
},
},
},
async (args) => {
const days = (args.days as number) || 7;
const league = args.league as string | undefined;
const apiKey = configManager.getConfig().footballApiKey;
try {
if (!apiKey) {
// Return placeholder information
return {
content: [
{
type: 'text',
text: `Football Matches (${days} days):\n\nNote: To get real-time match data, set FOOTBALL_API_KEY in environment variables.\n\nFree APIs available:\n- football-data.org (requires free API key)\n- api-football.com\n\nExample upcoming matches:\n- Premier League: Manchester United vs Liverpool (Tomorrow 15:00)\n- La Liga: Real Madrid vs Barcelona (Saturday 20:00)\n- Champions League: Various matches this week`,
},
],
};
}
// Using football-data.org API (free tier)
const today = new Date();
const dateFrom = new Date(today);
dateFrom.setDate(dateFrom.getDate() - days);
const dateTo = new Date(today);
dateTo.setDate(dateTo.getDate() + days);
const response = await axios.get(
`https://api.football-data.org/v4/matches`,
{
headers: {
'X-Auth-Token': apiKey,
},
params: {
dateFrom: dateFrom.toISOString().split('T')[0],
dateTo: dateTo.toISOString().split('T')[0],
competitions: league ? undefined : undefined, // Would need competition IDs
},
timeout: 5000,
}
);
const matches = response.data.matches || [];
if (matches.length === 0) {
return {
content: [
{
type: 'text',
text: `No matches found for the specified period.`,
},
],
};
}
const matchesText = matches
.slice(0, 20) // Limit to 20 matches
.map((match: any) => {
const home = match.homeTeam?.name || 'TBD';
const away = match.awayTeam?.name || 'TBD';
const date = new Date(match.utcDate).toLocaleString();
const status = match.status || 'SCHEDULED';
return `${home} vs ${away}\nDate: ${date}\nStatus: ${status}`;
})
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Football Matches (${matches.length} found):\n\n${matchesText}`,
},
],
};
} catch (error) {
logger.error('Error fetching football matches:', error);
return {
content: [
{
type: 'text',
text: `Error fetching matches: ${error instanceof Error ? error.message : String(error)}\n\nNote: Make sure FOOTBALL_API_KEY is set correctly.`,
},
],
isError: true,
};
}
}
);
// Get team information
mcpServer.registerTool(
{
name: 'football_team_info',
description: 'Get information about a football team',
inputSchema: {
type: 'object',
properties: {
team: {
type: 'string',
description: 'Team name',
},
},
required: ['team'],
},
},
async (args) => {
const team = args.team as string;
const apiKey = configManager.getConfig().footballApiKey;
try {
if (!apiKey) {
return {
content: [
{
type: 'text',
text: `Team Information: ${team}\n\nNote: To get real-time team data, set FOOTBALL_API_KEY in environment variables.\n\nYou can use football-data.org API (free tier available).`,
},
],
};
}
// Search for team
const searchResponse = await axios.get(
`https://api.football-data.org/v4/teams`,
{
headers: {
'X-Auth-Token': apiKey,
},
params: {
name: team,
},
timeout: 5000,
}
);
const teams = searchResponse.data.teams || [];
if (teams.length === 0) {
return {
content: [
{
type: 'text',
text: `Team "${team}" not found.`,
},
],
};
}
const teamData = teams[0];
const info = `Team: ${teamData.name}\nFounded: ${teamData.founded || 'N/A'}\nVenue: ${teamData.venue || 'N/A'}\nWebsite: ${teamData.website || 'N/A'}`;
return {
content: [
{
type: 'text',
text: info,
},
],
};
} catch (error) {
logger.error('Error fetching team info:', error);
return {
content: [
{
type: 'text',
text: `Error fetching team information: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
);
// Get league standings
mcpServer.registerTool(
{
name: 'football_standings',
description: 'Get league standings/table',
inputSchema: {
type: 'object',
properties: {
league: {
type: 'string',
description: 'League name or competition ID',
},
},
required: ['league'],
},
},
async (args) => {
const league = args.league as string;
const apiKey = configManager.getConfig().footballApiKey;
try {
if (!apiKey) {
return {
content: [
{
type: 'text',
text: `League Standings: ${league}\n\nNote: To get real-time standings, set FOOTBALL_API_KEY in environment variables.\n\nCommon leagues:\n- Premier League (2021)\n- La Liga (2014)\n- Bundesliga (2002)\n- Serie A (2019)\n- Ligue 1 (2015)\n- Champions League (2001)`,
},
],
};
}
// Note: This would require competition ID mapping
// For now, return placeholder
return {
content: [
{
type: 'text',
text: `League Standings for ${league}:\n\nNote: Full implementation requires competition ID mapping.\n\nYou can find competition IDs at:\nhttps://api.football-data.org/v4/competitions`,
},
],
};
} catch (error) {
logger.error('Error fetching standings:', error);
return {
content: [
{
type: 'text',
text: `Error fetching standings: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
);
}

315
src/tools/hobbies/games.ts Normal file
View File

@@ -0,0 +1,315 @@
/**
* Game information tools
*/
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { mcpServer } from '../../server.js';
import { database, GameWishlist } from '../../storage/database.js';
import { randomUUID } from 'crypto';
import axios from 'axios';
import { logger } from '../../utils/logger.js';
export function registerGameTools(): void {
// Get game information
mcpServer.registerTool(
{
name: 'game_info',
description: 'Get information about a video game',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Game name',
},
},
required: ['name'],
},
},
async (args) => {
const name = args.name as string;
try {
// Using RAWG.io API (free, no key required for basic usage)
const response = await axios.get('https://api.rawg.io/api/games', {
params: {
search: name,
page_size: 1,
},
timeout: 5000,
});
const games = response.data.results || [];
if (games.length === 0) {
return {
content: [
{
type: 'text',
text: `Game "${name}" not found.`,
},
],
};
}
const game = games[0];
const info = `Game: ${game.name}\nReleased: ${game.released || 'TBA'}\nRating: ${game.rating || 'N/A'}/5\nMetacritic: ${game.metacritic || 'N/A'}\nPlatforms: ${game.platforms?.map((p: any) => p.platform.name).join(', ') || 'N/A'}\nGenres: ${game.genres?.map((g: any) => g.name).join(', ') || 'N/A'}\nWebsite: ${game.website || 'N/A'}`;
return {
content: [
{
type: 'text',
text: info,
},
],
};
} catch (error) {
logger.error('Error fetching game info:', error);
return {
content: [
{
type: 'text',
text: `Error fetching game information: ${error instanceof Error ? error.message : String(error)}\n\nGame: ${name}\n\nNote: Using RAWG.io API for game information.`,
},
],
isError: true,
};
}
}
);
// Get game deals
mcpServer.registerTool(
{
name: 'game_deals',
description: 'Get game deals and discounts',
inputSchema: {
type: 'object',
properties: {
platform: {
type: 'string',
description: 'Platform (steam, epic, psn, xbox, switch)',
},
maxPrice: {
type: 'number',
description: 'Maximum price filter',
},
},
},
},
async (args) => {
const platform = args.platform as string | undefined;
const maxPrice = args.maxPrice as number | undefined;
try {
// Using CheapShark API (free, no key required)
const params: Record<string, string> = {};
if (platform) {
params.storeID = {
steam: '1',
epic: '25',
psn: '7',
xbox: '2',
switch: '6',
}[platform.toLowerCase()] || '1';
}
if (maxPrice) {
params.upperPrice = maxPrice.toString();
}
const response = await axios.get('https://www.cheapshark.com/api/1.0/deals', {
params: {
...params,
pageSize: 20,
sortBy: 'Deal Rating',
},
timeout: 5000,
});
const deals = response.data || [];
if (deals.length === 0) {
return {
content: [
{
type: 'text',
text: 'No deals found matching your criteria.',
},
],
};
}
const dealsText = deals
.map((deal: any) => {
return `${deal.title}\nPrice: $${deal.salePrice} (Was: $${deal.normalPrice})\nSavings: ${deal.savings}%\nStore: ${deal.storeID}\nLink: https://www.cheapshark.com/redirect?dealID=${deal.dealID}`;
})
.join('\n\n---\n\n');
return {
content: [
{
type: 'text',
text: `Game Deals (${deals.length} found):\n\n${dealsText}`,
},
],
};
} catch (error) {
logger.error('Error fetching game deals:', error);
return {
content: [
{
type: 'text',
text: `Error fetching game deals: ${error instanceof Error ? error.message : String(error)}\n\nNote: Using CheapShark API for game deals.`,
},
],
isError: true,
};
}
}
);
// Manage game wishlist
mcpServer.registerTool(
{
name: 'game_wishlist',
description: 'Add, list, or remove games from wishlist',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
description: 'Action: "add", "list", or "remove"',
enum: ['add', 'list', 'remove'],
},
gameName: {
type: 'string',
description: 'Game name (required for add/remove)',
},
platform: {
type: 'string',
description: 'Platform (optional)',
},
notes: {
type: 'string',
description: 'Notes about the game (optional)',
},
id: {
type: 'string',
description: 'Game ID (required for remove)',
},
},
required: ['action'],
},
},
async (args) => {
const action = args.action as string;
if (action === 'add') {
const gameName = args.gameName as string;
if (!gameName) {
return {
content: [
{
type: 'text',
text: 'Error: gameName is required for add action',
},
],
isError: true,
};
}
const game: GameWishlist = {
id: randomUUID(),
gameName,
platform: args.platform as string | undefined,
notes: args.notes as string | undefined,
addedAt: new Date().toISOString(),
};
database.saveGameWishlist(game);
return {
content: [
{
type: 'text',
text: `Game "${gameName}" added to wishlist with ID: ${game.id}`,
},
],
};
} else if (action === 'list') {
const games = database.getGameWishlist();
if (games.length === 0) {
return {
content: [
{
type: 'text',
text: 'Your wishlist is empty. Use game_wishlist with action="add" to add games.',
},
],
};
}
const list = games
.map(
(g) =>
`🎮 ${g.gameName}${g.platform ? ` (${g.platform})` : ''}\nID: ${g.id}${g.notes ? `\nNotes: ${g.notes}` : ''}\nAdded: ${new Date(g.addedAt).toLocaleDateString()}`
)
.join('\n\n---\n\n');
return {
content: [
{
type: 'text',
text: `Your Game Wishlist (${games.length} games):\n\n${list}`,
},
],
};
} else if (action === 'remove') {
const id = args.id as string;
if (!id) {
return {
content: [
{
type: 'text',
text: 'Error: id is required for remove action',
},
],
isError: true,
};
}
const deleted = database.deleteGameWishlist(id);
if (deleted) {
return {
content: [
{
type: 'text',
text: `Game with ID ${id} removed from wishlist`,
},
],
};
} else {
return {
content: [
{
type: 'text',
text: `Game with ID ${id} not found in wishlist`,
},
],
};
}
} else {
return {
content: [
{
type: 'text',
text: `Invalid action: ${action}. Use "add", "list", or "remove"`,
},
],
isError: true,
};
}
}
);
}

View File

@@ -0,0 +1,233 @@
/**
* Code review and optimization tools
*/
import { mcpServer } from "../../server.js";
export function registerCodeReviewTools(): void {
// Code review
mcpServer.registerTool(
{
name: "code_review",
description: "Review code and provide suggestions for improvement",
inputSchema: {
type: "object",
properties: {
code: {
type: "string",
description: "The code to review",
},
language: {
type: "string",
description: "Programming language",
},
},
required: ["code", "language"],
},
},
async (args) => {
const code = args.code as string;
const language = args.language as string;
// Basic code review analysis
const issues: string[] = [];
const suggestions: string[] = [];
// Check for common issues
if (code.includes("any")) {
issues.push('Found "any" type - consider using specific types');
}
if (code.includes("console.log")) {
suggestions.push(
"Consider removing console.log statements in production code"
);
}
if (code.includes("var ")) {
issues.push('Found "var" - prefer "let" or "const"');
}
if (code.match(/function\s+\w+\s*\(/)) {
suggestions.push("Consider using arrow functions for consistency");
}
// Check for error handling
if (
!code.includes("try") &&
!code.includes("catch") &&
code.includes("async")
) {
suggestions.push("Consider adding error handling for async operations");
}
// Check for TypeScript/Vue specific
if (language === "typescript" || language === "ts") {
if (code.includes("@ts-ignore") || code.includes("@ts-nocheck")) {
issues.push(
"Found TypeScript ignore comments - try to fix the underlying issues"
);
}
}
if (language === "vue" || language === "vue3") {
if (code.includes("Options API") && code.includes("setup()")) {
suggestions.push(
"Consider using Composition API with <script setup> for better TypeScript support"
);
}
}
let response = `Code Review for ${language} code:\n\n`;
if (issues.length > 0) {
response += `Issues Found:\n`;
issues.forEach((issue, i) => {
response += `${i + 1}. ${issue}\n`;
});
response += `\n`;
}
if (suggestions.length > 0) {
response += `Suggestions:\n`;
suggestions.forEach((suggestion, i) => {
response += `${i + 1}. ${suggestion}\n`;
});
response += `\n`;
}
if (issues.length === 0 && suggestions.length === 0) {
response += `No obvious issues found. Code looks good!\n`;
response += `General best practices:\n`;
response += `- Use TypeScript types strictly\n`;
response += `- Add error handling\n`;
response += `- Follow consistent code style\n`;
response += `- Add comments for complex logic\n`;
}
return {
content: [
{
type: "text",
text: response,
},
],
};
}
);
// Code optimization
mcpServer.registerTool(
{
name: "code_optimize",
description: "Provide code optimization suggestions",
inputSchema: {
type: "object",
properties: {
code: {
type: "string",
description: "The code to optimize",
},
language: {
type: "string",
description: "Programming language",
},
},
required: ["code", "language"],
},
},
async (args) => {
const code = args.code as string;
const language = args.language as string;
const optimizations: string[] = [];
// Performance optimizations
if (
code.includes(".map(") &&
code.includes(".filter(") &&
code.includes(".map(")
) {
optimizations.push(
"Consider combining multiple array operations to reduce iterations"
);
}
if (code.match(/for\s*\(\s*let\s+\w+\s*=\s*0/)) {
optimizations.push(
"Consider using for...of or array methods for better readability"
);
}
if (code.includes("==")) {
optimizations.push("Use === instead of == for strict equality checks");
}
// Vue-specific optimizations
if (language === "vue" || language === "vue3") {
if (code.includes("v-for") && !code.includes(":key")) {
optimizations.push(
"Add :key attribute to v-for for better performance"
);
}
if (code.includes("computed") && code.includes("watch")) {
optimizations.push(
"Consider using computed instead of watch when possible"
);
}
}
// TypeScript optimizations
if (language === "typescript" || language === "ts") {
if (code.includes("interface") && code.includes("type")) {
optimizations.push(
'Consider using "type" for unions/intersections, "interface" for object shapes'
);
}
}
// Bun/Node optimizations
if (language === "typescript" || language === "javascript") {
if (code.includes("require(")) {
optimizations.push(
"Consider using ES modules (import/export) instead of require"
);
}
if (
(code.includes("Promise.all") &&
code.match(/await\s+\w+\(\)/g)?.length) ||
0 > 3
) {
optimizations.push(
"Consider using Promise.all() for parallel async operations"
);
}
}
let response = `Code Optimization Suggestions for ${language}:\n\n`;
if (optimizations.length > 0) {
optimizations.forEach((opt, i) => {
response += `${i + 1}. ${opt}\n`;
});
} else {
response += `No obvious optimization opportunities found.\n\n`;
response += `General optimization tips:\n`;
response += `- Use appropriate data structures\n`;
response += `- Minimize re-renders (for UI frameworks)\n`;
response += `- Use memoization for expensive computations\n`;
response += `- Avoid unnecessary object creation in loops\n`;
response += `- Use async/await properly with Promise.all for parallel operations\n`;
}
return {
content: [
{
type: "text",
text: response,
},
],
};
}
);
}

View File

@@ -0,0 +1,228 @@
/**
* Code snippet management tools
*/
import { database, CodeSnippet } from "../../storage/database.js";
import { mcpServer } from "../../server.js";
import { randomUUID } from "crypto";
export function registerCodeSnippetTools(): void {
// Save code snippet
mcpServer.registerTool(
{
name: "code_snippet_save",
description:
"Save a code snippet with title, code, language, tags, and optional category",
inputSchema: {
type: "object",
properties: {
title: {
type: "string",
description: "Title of the code snippet",
},
code: {
type: "string",
description: "The code content",
},
language: {
type: "string",
description:
"Programming language (e.g., typescript, javascript, vue)",
},
tags: {
type: "array",
items: { type: "string" },
description: "Tags for categorization",
},
category: {
type: "string",
description: "Optional category (e.g., utils, components, api)",
},
id: {
type: "string",
description: "Optional ID for updating existing snippet",
},
},
required: ["title", "code", "language", "tags"],
},
},
async (args) => {
const now = new Date().toISOString();
const snippet: CodeSnippet = {
id: (args.id as string) || randomUUID(),
title: args.title as string,
code: args.code as string,
language: args.language as string,
tags: args.tags as string[],
category: args.category as string,
createdAt: args.id
? database.getCodeSnippet(args.id as string)?.createdAt || now
: now,
updatedAt: now,
};
database.saveCodeSnippet(snippet);
return {
content: [
{
type: "text",
text: `Code snippet "${snippet.title}" saved successfully with ID: ${snippet.id}`,
},
],
};
}
);
// Search code snippets
mcpServer.registerTool(
{
name: "code_snippet_search",
description: "Search code snippets by query and optional tags",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (searches in title, code, and language)",
},
tags: {
type: "array",
items: { type: "string" },
description: "Optional tags to filter by",
},
},
required: ["query"],
},
},
async (args) => {
const query = args.query as string;
const tags = args.tags as string[] | undefined;
const snippets = database.searchCodeSnippets(query, tags);
if (snippets.length === 0) {
return {
content: [
{
type: "text",
text: `No code snippets found matching "${query}"`,
},
],
};
}
const results = snippets
.map(
(s) =>
`ID: ${s.id}\nTitle: ${s.title}\nLanguage: ${
s.language
}\nTags: ${s.tags.join(", ")}\nCategory: ${
s.category || "N/A"
}\n\nCode:\n\`\`\`${s.language}\n${s.code}\n\`\`\`\n---`
)
.join("\n\n");
return {
content: [
{
type: "text",
text: `Found ${snippets.length} code snippet(s):\n\n${results}`,
},
],
};
}
);
// List all code snippets
mcpServer.registerTool(
{
name: "code_snippet_list",
description: "List all saved code snippets",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Maximum number of snippets to return",
},
},
},
},
async (args) => {
const snippets = database.getCodeSnippets();
const limit = args.limit as number | undefined;
const limited = limit ? snippets.slice(0, limit) : snippets;
if (limited.length === 0) {
return {
content: [
{
type: "text",
text: "No code snippets saved yet",
},
],
};
}
const list = limited
.map(
(s) =>
`- ${s.title} (${s.language}) - ID: ${s.id}\n Tags: ${s.tags.join(
", "
)}`
)
.join("\n");
return {
content: [
{
type: "text",
text: `Total: ${snippets.length} snippet(s)\n\n${list}`,
},
],
};
}
);
// Delete code snippet
mcpServer.registerTool(
{
name: "code_snippet_delete",
description: "Delete a code snippet by ID",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "ID of the code snippet to delete",
},
},
required: ["id"],
},
},
async (args) => {
const id = args.id as string;
const deleted = database.deleteCodeSnippet(id);
if (deleted) {
return {
content: [
{
type: "text",
text: `Code snippet with ID ${id} deleted successfully`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Code snippet with ID ${id} not found`,
},
],
};
}
}
);
}

View File

@@ -0,0 +1,185 @@
/**
* Technical documentation query tools
*/
import { mcpServer } from "../../server.js";
const DOCS_LINKS = {
typescript: {
official: "https://www.typescriptlang.org/docs/",
handbook: "https://www.typescriptlang.org/docs/handbook/intro.html",
api: "https://www.typescriptlang.org/docs/handbook/utility-types.html",
},
vue3: {
official: "https://vuejs.org/",
guide: "https://vuejs.org/guide/",
api: "https://vuejs.org/api/",
migration: "https://vuejs.org/guide/extras/migration-build.html",
},
bun: {
official: "https://bun.sh/docs",
runtime: "https://bun.sh/docs/runtime",
api: "https://bun.sh/docs/api",
test: "https://bun.sh/docs/test",
},
};
export function registerDocsTools(): void {
// TypeScript docs
mcpServer.registerTool(
{
name: "docs_typescript",
description: "Get TypeScript documentation links and information",
inputSchema: {
type: "object",
properties: {
topic: {
type: "string",
description:
'Specific topic to search for (e.g., "types", "interfaces", "generics")',
},
},
},
},
async (args) => {
const topic = args.topic as string | undefined;
const links = DOCS_LINKS.typescript;
let response = `TypeScript Documentation:\n\n`;
response += `Official Docs: ${links.official}\n`;
response += `Handbook: ${links.handbook}\n`;
response += `Utility Types API: ${links.api}\n\n`;
if (topic) {
response += `Searching for: ${topic}\n\n`;
response += `Common TypeScript topics:\n`;
response += `- Types & Interfaces: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html\n`;
response += `- Generics: https://www.typescriptlang.org/docs/handbook/2/generics.html\n`;
response += `- Classes: https://www.typescriptlang.org/docs/handbook/2/classes.html\n`;
response += `- Modules: https://www.typescriptlang.org/docs/handbook/2/modules.html\n`;
response += `- Type Guards: https://www.typescriptlang.org/docs/handbook/2/narrowing.html\n`;
} else {
response += `Quick Links:\n`;
response += `- Basic Types: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html\n`;
response += `- Advanced Types: https://www.typescriptlang.org/docs/handbook/2/types-from-types.html\n`;
response += `- Utility Types: https://www.typescriptlang.org/docs/handbook/utility-types.html\n`;
}
return {
content: [
{
type: "text",
text: response,
},
],
};
}
);
// Vue3 docs
mcpServer.registerTool(
{
name: "docs_vue3",
description: "Get Vue 3 documentation links and information",
inputSchema: {
type: "object",
properties: {
topic: {
type: "string",
description:
'Specific topic (e.g., "composition", "reactivity", "components")',
},
},
},
},
async (args) => {
const topic = args.topic as string | undefined;
const links = DOCS_LINKS.vue3;
let response = `Vue 3 Documentation:\n\n`;
response += `Official Site: ${links.official}\n`;
response += `Guide: ${links.guide}\n`;
response += `API Reference: ${links.api}\n\n`;
if (topic) {
response += `Searching for: ${topic}\n\n`;
response += `Common Vue 3 topics:\n`;
response += `- Composition API: https://vuejs.org/guide/extras/composition-api-faq.html\n`;
response += `- Reactivity: https://vuejs.org/guide/essentials/reactivity-fundamentals.html\n`;
response += `- Components: https://vuejs.org/guide/essentials/component-basics.html\n`;
response += `- Props: https://vuejs.org/guide/components/props.html\n`;
response += `- Events: https://vuejs.org/guide/components/events.html\n`;
response += `- Lifecycle: https://vuejs.org/guide/essentials/lifecycle.html\n`;
} else {
response += `Quick Links:\n`;
response += `- Getting Started: https://vuejs.org/guide/quick-start.html\n`;
response += `- Composition API: https://vuejs.org/guide/extras/composition-api-faq.html\n`;
response += `- Reactivity Fundamentals: https://vuejs.org/guide/essentials/reactivity-fundamentals.html\n`;
response += `- Components Basics: https://vuejs.org/guide/essentials/component-basics.html\n`;
}
return {
content: [
{
type: "text",
text: response,
},
],
};
}
);
// Bun docs
mcpServer.registerTool(
{
name: "docs_bun",
description: "Get Bun documentation links and information",
inputSchema: {
type: "object",
properties: {
topic: {
type: "string",
description:
'Specific topic (e.g., "runtime", "api", "test", "bundler")',
},
},
},
},
async (args) => {
const topic = args.topic as string | undefined;
const links = DOCS_LINKS.bun;
let response = `Bun Documentation:\n\n`;
response += `Official Docs: ${links.official}\n`;
response += `Runtime: ${links.runtime}\n`;
response += `API Reference: ${links.api}\n`;
response += `Testing: ${links.test}\n\n`;
if (topic) {
response += `Searching for: ${topic}\n\n`;
response += `Common Bun topics:\n`;
response += `- Runtime: https://bun.sh/docs/runtime\n`;
response += `- File System: https://bun.sh/docs/api/file-io\n`;
response += `- HTTP Server: https://bun.sh/docs/api/http\n`;
response += `- SQLite: https://bun.sh/docs/api/sqlite\n`;
response += `- Testing: https://bun.sh/docs/test\n`;
response += `- Bundler: https://bun.sh/docs/bundler\n`;
} else {
response += `Quick Links:\n`;
response += `- Getting Started: https://bun.sh/docs/installation\n`;
response += `- Runtime API: https://bun.sh/docs/runtime\n`;
response += `- HTTP Server: https://bun.sh/docs/api/http\n`;
response += `- File System: https://bun.sh/docs/api/file-io\n`;
}
return {
content: [
{
type: "text",
text: response,
},
],
};
}
);
}

View File

@@ -0,0 +1,622 @@
/**
* Project template generation tools
*/
import { mcpServer } from "../../server.js";
import { mkdirSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
export function registerProjectTemplateTools(): void {
// Create Vite + Vue3 project
mcpServer.registerTool(
{
name: "project_template_create",
description:
"Create a new Vite + Vue3 + TypeScript project with optional features",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Project name",
},
path: {
type: "string",
description:
"Path where to create the project (default: current directory)",
},
usePinia: {
type: "boolean",
description: "Include Pinia for state management",
default: false,
},
useRouter: {
type: "boolean",
description: "Include Vue Router",
default: false,
},
useTailwind: {
type: "boolean",
description: "Include Tailwind CSS",
default: false,
},
},
required: ["name"],
},
},
async (args) => {
const name = args.name as string;
const basePath = (args.path as string) || process.cwd();
const projectPath = join(basePath, name);
const usePinia = (args.usePinia as boolean) || false;
const useRouter = (args.useRouter as boolean) || false;
const useTailwind = (args.useTailwind as boolean) || false;
if (existsSync(projectPath)) {
return {
content: [
{
type: "text",
text: `Error: Directory ${projectPath} already exists`,
},
],
isError: true,
};
}
try {
mkdirSync(projectPath, { recursive: true });
mkdirSync(join(projectPath, "src"), { recursive: true });
mkdirSync(join(projectPath, "src", "components"), { recursive: true });
// package.json
const dependencies: Record<string, string> = {
vue: "^3.4.0",
};
const devDependencies: Record<string, string> = {
"@vitejs/plugin-vue": "^5.0.0",
typescript: "^5.6.3",
vite: "^5.4.0",
"vue-tsc": "^2.0.0",
};
if (usePinia) {
dependencies.pinia = "^2.1.7";
}
if (useRouter) {
dependencies["vue-router"] = "^4.3.0";
}
if (useTailwind) {
devDependencies.tailwindcss = "^3.4.0";
devDependencies.autoprefixer = "^10.4.18";
devDependencies.postcss = "^8.4.35";
}
const packageJson = {
name,
version: "0.1.0",
type: "module",
scripts: {
dev: "vite",
build: "vue-tsc && vite build",
preview: "vite preview",
},
dependencies,
devDependencies,
};
writeFileSync(
join(projectPath, "package.json"),
JSON.stringify(packageJson, null, 2)
);
// tsconfig.json
const tsconfig = {
compilerOptions: {
target: "ES2020",
useDefineForClassFields: true,
module: "ESNext",
lib: ["ES2020", "DOM", "DOM.Iterable"],
skipLibCheck: true,
moduleResolution: "bundler",
allowImportingTsExtensions: true,
resolveJsonModule: true,
isolatedModules: true,
noEmit: true,
jsx: "preserve",
strict: true,
noUnusedLocals: true,
noUnusedParameters: true,
noFallthroughCasesInSwitch: true,
},
include: ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
references: [{ path: "./tsconfig.node.json" }],
};
writeFileSync(
join(projectPath, "tsconfig.json"),
JSON.stringify(tsconfig, null, 2)
);
// vite.config.ts
let viteConfig = `import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
})
`;
writeFileSync(join(projectPath, "vite.config.ts"), viteConfig);
// index.html
const indexHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${name}</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
`;
writeFileSync(join(projectPath, "index.html"), indexHtml);
// src/main.ts
let mainTs = `import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
`;
if (usePinia) {
mainTs += `import { createPinia } from 'pinia'\n\n`;
}
if (useRouter) {
mainTs += `import router from './router'\n\n`;
}
mainTs += `const app = createApp(App)\n`;
if (usePinia) {
mainTs += `app.use(createPinia())\n`;
}
if (useRouter) {
mainTs += `app.use(router)\n`;
}
mainTs += `app.mount('#app')\n`;
writeFileSync(join(projectPath, "src", "main.ts"), mainTs);
// src/App.vue
const appVue = `<script setup lang="ts">
// Your app logic here
</script>
<template>
<div id="app">
<h1>Welcome to ${name}</h1>
</div>
</template>
<style scoped>
#app {
text-align: center;
}
</style>
`;
writeFileSync(join(projectPath, "src", "App.vue"), appVue);
// src/style.css
writeFileSync(join(projectPath, "src", "style.css"), "");
// .gitignore
const gitignore = `node_modules
dist
dist-ssr
*.local
.DS_Store
`;
writeFileSync(join(projectPath, ".gitignore"), gitignore);
// README.md
const readme = `# ${name}
A Vue 3 + TypeScript + Vite project.
## Setup
\`\`\`bash
bun install
\`\`\`
## Development
\`\`\`bash
bun run dev
\`\`\`
## Build
\`\`\`bash
bun run build
\`\`\`
`;
writeFileSync(join(projectPath, "README.md"), readme);
const features = [];
if (usePinia) features.push("Pinia");
if (useRouter) features.push("Vue Router");
if (useTailwind) features.push("Tailwind CSS");
return {
content: [
{
type: "text",
text: `Project "${name}" created successfully at ${projectPath}\n\nFeatures: ${
features.length > 0 ? features.join(", ") : "Basic setup"
}\n\nNext steps:\n1. cd ${name}\n2. bun install\n3. bun run dev`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating project: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
// Create fullstack project
mcpServer.registerTool(
{
name: "project_template_create_fullstack",
description:
"Create a fullstack project with Vite+Vue3 frontend and Bun backend",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Project name",
},
path: {
type: "string",
description: "Path where to create the project",
},
},
required: ["name"],
},
},
async (args) => {
const name = args.name as string;
const basePath = (args.path as string) || process.cwd();
const projectPath = join(basePath, name);
if (existsSync(projectPath)) {
return {
content: [
{
type: "text",
text: `Error: Directory ${projectPath} already exists`,
},
],
isError: true,
};
}
try {
mkdirSync(projectPath, { recursive: true });
mkdirSync(join(projectPath, "frontend"), { recursive: true });
mkdirSync(join(projectPath, "backend"), { recursive: true });
mkdirSync(join(projectPath, "backend", "src"), { recursive: true });
// Root package.json
const rootPackageJson = {
name: `${name}-root`,
version: "0.1.0",
private: true,
scripts: {
dev: 'concurrently "bun run --cwd frontend dev" "bun run --cwd backend dev"',
"dev:frontend": "bun run --cwd frontend dev",
"dev:backend": "bun run --cwd backend dev",
},
devDependencies: {
concurrently: "^8.2.2",
},
};
writeFileSync(
join(projectPath, "package.json"),
JSON.stringify(rootPackageJson, null, 2)
);
// Frontend package.json
const frontendPackageJson = {
name: `${name}-frontend`,
version: "0.1.0",
type: "module",
scripts: {
dev: "vite",
build: "vue-tsc && vite build",
preview: "vite preview",
},
dependencies: {
vue: "^3.4.0",
axios: "^1.7.7",
},
devDependencies: {
"@vitejs/plugin-vue": "^5.0.0",
typescript: "^5.6.3",
vite: "^5.4.0",
"vue-tsc": "^2.0.0",
},
};
writeFileSync(
join(projectPath, "frontend", "package.json"),
JSON.stringify(frontendPackageJson, null, 2)
);
// Backend package.json
const backendPackageJson = {
name: `${name}-backend`,
version: "0.1.0",
type: "module",
scripts: {
dev: "bun run src/index.ts",
build: "bun build src/index.ts --outdir dist",
start: "bun run dist/index.js",
},
dependencies: {
"@hono/node-server": "^1.12.0",
hono: "^4.6.0",
},
devDependencies: {
"@types/node": "^22.7.9",
typescript: "^5.6.3",
},
};
writeFileSync(
join(projectPath, "backend", "package.json"),
JSON.stringify(backendPackageJson, null, 2)
);
// Backend src/index.ts
const backendIndex = `import { Hono } from 'hono'
import { serve } from '@hono/node-server'
const app = new Hono()
app.get('/', (c) => {
return c.json({ message: 'Hello from backend!' })
})
app.get('/api/health', (c) => {
return c.json({ status: 'ok' })
})
const port = 3000
console.log(\`Server is running on port \${port}\`)
serve({
fetch: app.fetch,
port,
})
`;
writeFileSync(
join(projectPath, "backend", "src", "index.ts"),
backendIndex
);
// Backend tsconfig.json
const backendTsconfig = {
compilerOptions: {
target: "ES2022",
module: "ESNext",
lib: ["ES2022"],
moduleResolution: "bundler",
types: ["bun-types"],
strict: true,
esModuleInterop: true,
skipLibCheck: true,
},
include: ["src/**/*"],
};
writeFileSync(
join(projectPath, "backend", "tsconfig.json"),
JSON.stringify(backendTsconfig, null, 2)
);
// Frontend vite.config.ts
const frontendViteConfig = `import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
})
`;
writeFileSync(
join(projectPath, "frontend", "vite.config.ts"),
frontendViteConfig
);
// Frontend src/main.ts
const frontendMain = `import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
`;
writeFileSync(
join(projectPath, "frontend", "src", "main.ts"),
frontendMain
);
// Frontend src/App.vue
const frontendApp = `<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
const message = ref('Loading...')
onMounted(async () => {
try {
const res = await axios.get('/api/health')
message.value = res.data.message || 'Connected to backend!'
} catch (error) {
message.value = 'Failed to connect to backend'
}
})
</script>
<template>
<div id="app">
<h1>{{ message }}</h1>
</div>
</template>
<style scoped>
#app {
text-align: center;
padding: 2rem;
}
</style>
`;
writeFileSync(
join(projectPath, "frontend", "src", "App.vue"),
frontendApp
);
// Frontend index.html
const frontendIndexHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${name}</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
`;
writeFileSync(
join(projectPath, "frontend", "index.html"),
frontendIndexHtml
);
writeFileSync(join(projectPath, "frontend", "src", "style.css"), "");
// README
const readme = `# ${name}
Fullstack project with Vue 3 frontend and Bun backend.
## Setup
\`\`\`bash
bun install
bun run --cwd frontend install
bun run --cwd backend install
\`\`\`
## Development
\`\`\`bash
bun run dev
\`\`\`
This will start both frontend (port 5173) and backend (port 3000) concurrently.
`;
writeFileSync(join(projectPath, "README.md"), readme);
return {
content: [
{
type: "text",
text: `Fullstack project "${name}" created successfully at ${projectPath}\n\nStructure:\n- frontend/ (Vite + Vue3)\n- backend/ (Bun + Hono)\n\nNext steps:\n1. cd ${name}\n2. bun install\n3. bun run --cwd frontend install\n4. bun run --cwd backend install\n5. bun run dev`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating project: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
// List templates
mcpServer.registerTool(
{
name: "project_template_list",
description: "List available project templates",
inputSchema: {
type: "object",
properties: {},
},
},
async () => {
return {
content: [
{
type: "text",
text: `Available project templates:
1. **Vite + Vue3 + TypeScript** (project_template_create)
- Basic Vue 3 setup with TypeScript
- Optional: Pinia, Vue Router, Tailwind CSS
2. **Fullstack** (project_template_create_fullstack)
- Frontend: Vite + Vue3 + TypeScript
- Backend: Bun + Hono
- Includes proxy configuration for API calls
Use the respective tool to create a project.`,
},
],
};
}
);
}

45
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,45 @@
/**
* Simple logger utility
*/
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
class Logger {
private level: LogLevel = LogLevel.INFO;
setLevel(level: LogLevel): void {
this.level = level;
}
debug(message: string, ...args: unknown[]): void {
if (this.level <= LogLevel.DEBUG) {
console.debug(`[DEBUG] ${message}`, ...args);
}
}
info(message: string, ...args: unknown[]): void {
if (this.level <= LogLevel.INFO) {
console.info(`[INFO] ${message}`, ...args);
}
}
warn(message: string, ...args: unknown[]): void {
if (this.level <= LogLevel.WARN) {
console.warn(`[WARN] ${message}`, ...args);
}
}
error(message: string, ...args: unknown[]): void {
if (this.level <= LogLevel.ERROR) {
console.error(`[ERROR] ${message}`, ...args);
}
}
}
export const logger = new Logger();