feat: 初次提交
This commit is contained in:
60
src/index.ts
Normal file
60
src/index.ts
Normal 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
87
src/server.ts
Normal 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
87
src/storage/config.ts
Normal 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
282
src/storage/database.ts
Normal 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
212
src/tools/common/notes.ts
Normal 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
168
src/tools/common/tasks.ts
Normal 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
296
src/tools/devops/nas.ts
Normal 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
204
src/tools/devops/router.ts
Normal 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
367
src/tools/devops/server.ts
Normal 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
182
src/tools/family/baby.ts
Normal 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
234
src/tools/family/math.ts
Normal 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}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
259
src/tools/hobbies/football.ts
Normal file
259
src/tools/hobbies/football.ts
Normal 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
315
src/tools/hobbies/games.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
233
src/tools/programming/codeReview.ts
Normal file
233
src/tools/programming/codeReview.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
228
src/tools/programming/codeSnippet.ts
Normal file
228
src/tools/programming/codeSnippet.ts
Normal 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`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
185
src/tools/programming/docs.ts
Normal file
185
src/tools/programming/docs.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
622
src/tools/programming/projectTemplate.ts
Normal file
622
src/tools/programming/projectTemplate.ts
Normal 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
45
src/utils/logger.ts
Normal 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();
|
||||
|
||||
Reference in New Issue
Block a user