feat: Migrate data storage to PostgreSQL with schema setup, initialization script, and update documentation
This commit is contained in:
@@ -28,9 +28,6 @@ RUN bun install --frozen-lockfile --production
|
|||||||
COPY --from=base /app/dist ./dist
|
COPY --from=base /app/dist ./dist
|
||||||
COPY --from=base /app/src ./src
|
COPY --from=base /app/src ./src
|
||||||
|
|
||||||
# Create data directory for persistent storage
|
|
||||||
RUN mkdir -p /app/data
|
|
||||||
|
|
||||||
# Expose port (if needed for health checks)
|
# Expose port (if needed for health checks)
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -176,13 +176,35 @@ Example configuration for Claude Desktop (`claude_desktop_config.json`):
|
|||||||
|
|
||||||
## Data Storage
|
## Data Storage
|
||||||
|
|
||||||
All data is stored locally in JSON files in the `data/` directory:
|
All data is stored in PostgreSQL database. The application requires a PostgreSQL database connection.
|
||||||
- `codeSnippets.json` - Code snippets
|
|
||||||
- `notes.json` - Personal notes
|
### Database Setup
|
||||||
- `tasks.json` - Tasks
|
|
||||||
- `babyMilestones.json` - Baby milestones
|
1. **Configure Database Connection**
|
||||||
- `mathResources.json` - Math resources
|
|
||||||
- `gameWishlist.json` - Game wishlist
|
Set the `DATABASE_URL` environment variable in your `.env` file:
|
||||||
|
```
|
||||||
|
DATABASE_URL=postgresql://user:password@host:port/database
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Initialize Database Schema**
|
||||||
|
|
||||||
|
Run the initialization script to create all required tables:
|
||||||
|
```bash
|
||||||
|
bun run init-db
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create the following tables:
|
||||||
|
- `code_snippets` - Code snippets
|
||||||
|
- `notes` - Personal notes
|
||||||
|
- `tasks` - Tasks
|
||||||
|
- `baby_milestones` - Baby milestones
|
||||||
|
- `math_resources` - Math resources
|
||||||
|
- `game_wishlist` - Game wishlist
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
|
||||||
|
The database schema is defined in `src/storage/schema.sql`. All tables include appropriate indexes for optimal query performance.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,21 @@ services:
|
|||||||
container_name: cloud-mcp
|
container_name: cloud-mcp
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
# Mount data directory for persistent storage
|
|
||||||
- ./data:/app/data
|
|
||||||
# Mount .env file if exists (optional, can use environment variables instead)
|
# Mount .env file if exists (optional, can use environment variables instead)
|
||||||
- ./.env:/app/.env:ro
|
- ./.env:/app/.env:ro
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
|
# PostgreSQL connection (required)
|
||||||
|
# - DATABASE_URL=${DATABASE_URL}
|
||||||
# Add your environment variables here or use .env file
|
# Add your environment variables here or use .env file
|
||||||
# - NAS_HOST=${NAS_HOST}
|
# - NAS_HOST=${NAS_HOST}
|
||||||
# - SERVER_HOST=${SERVER_HOST}
|
# - SERVER_HOST=${SERVER_HOST}
|
||||||
# etc.
|
# etc.
|
||||||
|
# Note: This service requires an external PostgreSQL database.
|
||||||
|
# Set DATABASE_URL environment variable to connect to your PostgreSQL instance.
|
||||||
|
# For local development, you can uncomment the postgres service below:
|
||||||
|
# depends_on:
|
||||||
|
# - postgres
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
# Health check (optional)
|
# Health check (optional)
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ EMAIL_PASSWORD=your-app-password
|
|||||||
EMAIL_FROM=Your Name
|
EMAIL_FROM=Your Name
|
||||||
EMAIL_SECURE=false
|
EMAIL_SECURE=false
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
# PostgreSQL 数据库连接配置
|
||||||
|
DATABASE_URL=postgresql://user:password@host:port/database
|
||||||
|
|
||||||
# API Keys (optional)
|
# API Keys (optional)
|
||||||
# 可选:配置 API 密钥以使用完整功能
|
# 可选:配置 API 密钥以使用完整功能
|
||||||
# 足球信息 API (football-data.org - 免费注册获取)
|
# 足球信息 API (football-data.org - 免费注册获取)
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
"restart:pm2": "pm2 restart cloud-mcp",
|
"restart:pm2": "pm2 restart cloud-mcp",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"test:watch": "bun test --watch",
|
"test:watch": "bun test --watch",
|
||||||
"test:coverage": "bun test --coverage"
|
"test:coverage": "bun test --coverage",
|
||||||
|
"init-db": "bun run scripts/init-db.ts"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mcp",
|
"mcp",
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"nodemailer": "^6.9.8",
|
"nodemailer": "^6.9.8",
|
||||||
|
"postgres": "^3.4.3",
|
||||||
"ssh2": "^1.15.0"
|
"ssh2": "^1.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
81
scripts/init-db.ts
Normal file
81
scripts/init-db.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Database initialization script
|
||||||
|
* Creates all required tables in PostgreSQL
|
||||||
|
*/
|
||||||
|
|
||||||
|
import postgres from "postgres";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
async function initDatabase() {
|
||||||
|
const dbUrl = process.env.DATABASE_URL;
|
||||||
|
|
||||||
|
if (!dbUrl) {
|
||||||
|
console.error("Error: DATABASE_URL environment variable is required");
|
||||||
|
console.error("Please set it in your .env file or export it:");
|
||||||
|
console.error(" export DATABASE_URL=postgresql://user:password@host:port/database");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Connecting to PostgreSQL...");
|
||||||
|
const sql = postgres(dbUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test connection
|
||||||
|
await sql`SELECT 1`;
|
||||||
|
console.log("✓ Connected to PostgreSQL");
|
||||||
|
|
||||||
|
// Read schema file
|
||||||
|
const schemaPath = join(process.cwd(), "src", "storage", "schema.sql");
|
||||||
|
console.log(`Reading schema from ${schemaPath}...`);
|
||||||
|
const schema = readFileSync(schemaPath, "utf-8");
|
||||||
|
|
||||||
|
// Split by semicolons and execute each statement
|
||||||
|
const statements = schema
|
||||||
|
.split(";")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0 && !s.startsWith("--"));
|
||||||
|
|
||||||
|
console.log(`Executing ${statements.length} SQL statements...`);
|
||||||
|
|
||||||
|
for (const statement of statements) {
|
||||||
|
if (statement) {
|
||||||
|
try {
|
||||||
|
// Use postgres.unsafe() to execute raw SQL
|
||||||
|
await (sql as any).unsafe(statement);
|
||||||
|
console.log(`✓ Executed: ${statement.substring(0, 50)}...`);
|
||||||
|
} catch (error) {
|
||||||
|
// Check if it's a "already exists" error (which is OK)
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
|
if (errorMessage.includes("already exists") || errorMessage.includes("duplicate")) {
|
||||||
|
console.log(`⚠ Skipped (already exists): ${statement.substring(0, 50)}...`);
|
||||||
|
} else {
|
||||||
|
console.error(`✗ Error executing statement: ${errorMessage}`);
|
||||||
|
console.error(` Statement: ${statement.substring(0, 100)}...`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✓ Database initialization completed successfully!");
|
||||||
|
console.log("\nTables created:");
|
||||||
|
console.log(" - code_snippets");
|
||||||
|
console.log(" - notes");
|
||||||
|
console.log(" - tasks");
|
||||||
|
console.log(" - baby_milestones");
|
||||||
|
console.log(" - math_resources");
|
||||||
|
console.log(" - game_wishlist");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("\n✗ Database initialization failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await sql.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run initialization
|
||||||
|
initDatabase().catch((error) => {
|
||||||
|
console.error("Fatal error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
31
src/index.ts
31
src/index.ts
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { mcpServer } from "./server.js";
|
import { mcpServer } from "./server.js";
|
||||||
import { logger } from "./utils/logger.js";
|
import { logger } from "./utils/logger.js";
|
||||||
|
import { database } from "./storage/database.js";
|
||||||
|
|
||||||
// Register all tools
|
// Register all tools
|
||||||
import { registerCodeSnippetTools } from "./tools/programming/codeSnippet.js";
|
import { registerCodeSnippetTools } from "./tools/programming/codeSnippet.js";
|
||||||
@@ -57,10 +58,30 @@ registerNoteTools();
|
|||||||
registerTaskTools();
|
registerTaskTools();
|
||||||
registerEmailTools();
|
registerEmailTools();
|
||||||
|
|
||||||
logger.info("All tools registered. Starting MCP server...");
|
logger.info("All tools registered. Initializing database...");
|
||||||
|
|
||||||
// Start the server
|
// Initialize database connection
|
||||||
mcpServer.start().catch((error) => {
|
database
|
||||||
logger.error("Failed to start MCP server:", error);
|
.initialize()
|
||||||
process.exit(1);
|
.then(() => {
|
||||||
|
logger.info("Database connected successfully. Starting MCP server...");
|
||||||
|
// Start the server
|
||||||
|
return mcpServer.start();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error("Failed to initialize database or start MCP server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on("SIGINT", async () => {
|
||||||
|
logger.info("Shutting down...");
|
||||||
|
await database.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", async () => {
|
||||||
|
logger.info("Shutting down...");
|
||||||
|
await database.close();
|
||||||
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Database/storage layer for the MCP server
|
* Database/storage layer for the MCP server
|
||||||
* Uses JSON file storage for simplicity
|
* Uses PostgreSQL for data persistence
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
import postgres from "postgres";
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
// Use environment variable for test data directory, or default to project data directory
|
|
||||||
const getDataDir = () => {
|
|
||||||
if (process.env.MCP_TEST_DATA_DIR) {
|
|
||||||
return process.env.MCP_TEST_DATA_DIR;
|
|
||||||
}
|
|
||||||
return join(process.cwd(), "data");
|
|
||||||
};
|
|
||||||
|
|
||||||
const DATA_DIR = getDataDir();
|
|
||||||
|
|
||||||
|
// Export interfaces (keep them unchanged for compatibility)
|
||||||
export interface CodeSnippet {
|
export interface CodeSnippet {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -72,222 +62,433 @@ export interface GameWishlist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
private getDataDir(): string {
|
private sql: postgres.Sql | null = null;
|
||||||
return getDataDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureDataDir(): void {
|
private getConnectionString(): string {
|
||||||
const dataDir = this.getDataDir();
|
// For tests, use test database URL if provided
|
||||||
if (!existsSync(dataDir)) {
|
const testUrl = process.env.MCP_TEST_DATABASE_URL;
|
||||||
mkdirSync(dataDir, { recursive: true });
|
if (testUrl) {
|
||||||
|
return testUrl;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const dbUrl = process.env.DATABASE_URL;
|
||||||
private getFilePath(collection: string): string {
|
if (!dbUrl) {
|
||||||
this.ensureDataDir();
|
throw new Error(
|
||||||
return join(this.getDataDir(), `${collection}.json`);
|
"DATABASE_URL environment variable is required. Please set it in your .env file."
|
||||||
}
|
);
|
||||||
|
|
||||||
private readCollection<T>(collection: string): T[] {
|
|
||||||
const filePath = this.getFilePath(collection);
|
|
||||||
if (!existsSync(filePath)) {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
return dbUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSql(): postgres.Sql {
|
||||||
|
if (!this.sql) {
|
||||||
|
const connectionString = this.getConnectionString();
|
||||||
|
this.sql = postgres(connectionString, {
|
||||||
|
max: 10, // Connection pool size
|
||||||
|
idle_timeout: 20,
|
||||||
|
connect_timeout: 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
// Test connection
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(filePath, "utf-8");
|
await this.getSql()`SELECT 1`;
|
||||||
return JSON.parse(content);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error reading ${collection}:`, error);
|
console.error("Failed to connect to PostgreSQL:", error);
|
||||||
return [];
|
throw new Error(
|
||||||
|
`Database connection failed: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private writeCollection<T>(collection: string, data: T[]): void {
|
async close(): Promise<void> {
|
||||||
const filePath = this.getFilePath(collection);
|
if (this.sql) {
|
||||||
try {
|
await this.sql.end();
|
||||||
writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
this.sql = null;
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error writing ${collection}:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code Snippets
|
// Code Snippets
|
||||||
saveCodeSnippet(snippet: CodeSnippet): void {
|
async saveCodeSnippet(snippet: CodeSnippet): Promise<void> {
|
||||||
const snippets = this.readCollection<CodeSnippet>("codeSnippets");
|
const sql = this.getSql();
|
||||||
const index = snippets.findIndex((s) => s.id === snippet.id);
|
await sql`
|
||||||
if (index >= 0) {
|
INSERT INTO code_snippets (id, title, code, language, tags, category, created_at, updated_at)
|
||||||
snippets[index] = { ...snippet, updatedAt: new Date().toISOString() };
|
VALUES (${snippet.id}, ${snippet.title}, ${snippet.code}, ${snippet.language}, ${snippet.tags}, ${snippet.category}, ${snippet.createdAt}, ${snippet.updatedAt})
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
code = EXCLUDED.code,
|
||||||
|
language = EXCLUDED.language,
|
||||||
|
tags = EXCLUDED.tags,
|
||||||
|
category = EXCLUDED.category,
|
||||||
|
updated_at = EXCLUDED.updated_at
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCodeSnippets(): Promise<CodeSnippet[]> {
|
||||||
|
const sql = this.getSql();
|
||||||
|
const rows = await sql`
|
||||||
|
SELECT id, title, code, language, tags, category, created_at, updated_at
|
||||||
|
FROM code_snippets
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
`;
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
code: row.code,
|
||||||
|
language: row.language,
|
||||||
|
tags: row.tags || [],
|
||||||
|
category: row.category || undefined,
|
||||||
|
createdAt: row.created_at.toISOString(),
|
||||||
|
updatedAt: row.updated_at.toISOString(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCodeSnippet(id: string): Promise<CodeSnippet | undefined> {
|
||||||
|
const sql = this.getSql();
|
||||||
|
const rows = await sql`
|
||||||
|
SELECT id, title, code, language, tags, category, created_at, updated_at
|
||||||
|
FROM code_snippets
|
||||||
|
WHERE id = ${id}
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const row = rows[0];
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
code: row.code,
|
||||||
|
language: row.language,
|
||||||
|
tags: row.tags || [],
|
||||||
|
category: row.category || undefined,
|
||||||
|
createdAt: row.created_at.toISOString(),
|
||||||
|
updatedAt: row.updated_at.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCodeSnippet(id: string): Promise<boolean> {
|
||||||
|
const sql = this.getSql();
|
||||||
|
const result = await sql`DELETE FROM code_snippets WHERE id = ${id}`;
|
||||||
|
return result.count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchCodeSnippets(query: string, tags?: string[]): Promise<CodeSnippet[]> {
|
||||||
|
const sql = this.getSql();
|
||||||
|
const searchPattern = `%${query}%`;
|
||||||
|
let rows;
|
||||||
|
|
||||||
|
if (tags && tags.length > 0) {
|
||||||
|
rows = await sql`
|
||||||
|
SELECT id, title, code, language, tags, category, created_at, updated_at
|
||||||
|
FROM code_snippets
|
||||||
|
WHERE (
|
||||||
|
title ILIKE ${searchPattern} OR
|
||||||
|
code ILIKE ${searchPattern} OR
|
||||||
|
language ILIKE ${searchPattern}
|
||||||
|
) AND tags && ${tags}
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
snippets.push(snippet);
|
rows = await sql`
|
||||||
|
SELECT id, title, code, language, tags, category, created_at, updated_at
|
||||||
|
FROM code_snippets
|
||||||
|
WHERE title ILIKE ${searchPattern} OR code ILIKE ${searchPattern} OR language ILIKE ${searchPattern}
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
this.writeCollection("codeSnippets", snippets);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCodeSnippets(): CodeSnippet[] {
|
return rows.map((row) => ({
|
||||||
return this.readCollection<CodeSnippet>("codeSnippets");
|
id: row.id,
|
||||||
}
|
title: row.title,
|
||||||
|
code: row.code,
|
||||||
getCodeSnippet(id: string): CodeSnippet | undefined {
|
language: row.language,
|
||||||
const snippets = this.readCollection<CodeSnippet>("codeSnippets");
|
tags: row.tags || [],
|
||||||
return snippets.find((s) => s.id === id);
|
category: row.category || undefined,
|
||||||
}
|
createdAt: row.created_at.toISOString(),
|
||||||
|
updatedAt: row.updated_at.toISOString(),
|
||||||
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
|
// Notes
|
||||||
saveNote(note: Note): void {
|
async saveNote(note: Note): Promise<void> {
|
||||||
const notes = this.readCollection<Note>("notes");
|
const sql = this.getSql();
|
||||||
const index = notes.findIndex((n) => n.id === note.id);
|
await sql`
|
||||||
if (index >= 0) {
|
INSERT INTO notes (id, title, content, tags, created_at, updated_at)
|
||||||
notes[index] = { ...note, updatedAt: new Date().toISOString() };
|
VALUES (${note.id}, ${note.title}, ${note.content}, ${note.tags}, ${note.createdAt}, ${note.updatedAt})
|
||||||
} else {
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
notes.push(note);
|
title = EXCLUDED.title,
|
||||||
|
content = EXCLUDED.content,
|
||||||
|
tags = EXCLUDED.tags,
|
||||||
|
updated_at = EXCLUDED.updated_at
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNotes(): Promise<Note[]> {
|
||||||
|
const sql = this.getSql();
|
||||||
|
const rows = await sql`
|
||||||
|
SELECT id, title, content, tags, created_at, updated_at
|
||||||
|
FROM notes
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
`;
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
content: row.content,
|
||||||
|
tags: row.tags || [],
|
||||||
|
createdAt: row.created_at.toISOString(),
|
||||||
|
updatedAt: row.updated_at.toISOString(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNote(id: string): Promise<Note | undefined> {
|
||||||
|
const sql = this.getSql();
|
||||||
|
const rows = await sql`
|
||||||
|
SELECT id, title, content, tags, created_at, updated_at
|
||||||
|
FROM notes
|
||||||
|
WHERE id = ${id}
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
this.writeCollection("notes", notes);
|
const row = rows[0];
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
content: row.content,
|
||||||
|
tags: row.tags || [],
|
||||||
|
createdAt: row.created_at.toISOString(),
|
||||||
|
updatedAt: row.updated_at.toISOString(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotes(): Note[] {
|
async searchNotes(query: string): Promise<Note[]> {
|
||||||
return this.readCollection<Note>("notes");
|
const sql = this.getSql();
|
||||||
|
const searchPattern = `%${query}%`;
|
||||||
|
const rows = await sql`
|
||||||
|
SELECT id, title, content, tags, created_at, updated_at
|
||||||
|
FROM notes
|
||||||
|
WHERE title ILIKE ${searchPattern} OR content ILIKE ${searchPattern} OR EXISTS (
|
||||||
|
SELECT 1 FROM unnest(tags) AS tag WHERE tag ILIKE ${searchPattern}
|
||||||
|
)
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
`;
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
content: row.content,
|
||||||
|
tags: row.tags || [],
|
||||||
|
createdAt: row.created_at.toISOString(),
|
||||||
|
updatedAt: row.updated_at.toISOString(),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getNote(id: string): Note | undefined {
|
async deleteNote(id: string): Promise<boolean> {
|
||||||
const notes = this.readCollection<Note>("notes");
|
const sql = this.getSql();
|
||||||
return notes.find((n) => n.id === id);
|
const result = await sql`DELETE FROM notes WHERE id = ${id}`;
|
||||||
}
|
return result.count > 0;
|
||||||
|
|
||||||
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
|
// Tasks
|
||||||
saveTask(task: Task): void {
|
async saveTask(task: Task): Promise<void> {
|
||||||
const tasks = this.readCollection<Task>("tasks");
|
const sql = this.getSql();
|
||||||
const index = tasks.findIndex((t) => t.id === task.id);
|
await sql`
|
||||||
if (index >= 0) {
|
INSERT INTO tasks (id, title, description, completed, created_at, completed_at)
|
||||||
tasks[index] = task;
|
VALUES (${task.id}, ${task.title}, ${task.description}, ${task.completed}, ${task.createdAt}, ${task.completedAt || null})
|
||||||
} else {
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
tasks.push(task);
|
title = EXCLUDED.title,
|
||||||
}
|
description = EXCLUDED.description,
|
||||||
this.writeCollection("tasks", tasks);
|
completed = EXCLUDED.completed,
|
||||||
|
completed_at = EXCLUDED.completed_at
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTasks(completed?: boolean): Task[] {
|
async getTasks(completed?: boolean): Promise<Task[]> {
|
||||||
const tasks = this.readCollection<Task>("tasks");
|
const sql = this.getSql();
|
||||||
|
let rows;
|
||||||
if (completed === undefined) {
|
if (completed === undefined) {
|
||||||
return tasks;
|
rows = await sql`
|
||||||
|
SELECT id, title, description, completed, created_at, completed_at
|
||||||
|
FROM tasks
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
rows = await sql`
|
||||||
|
SELECT id, title, description, completed, created_at, completed_at
|
||||||
|
FROM tasks
|
||||||
|
WHERE completed = ${completed}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
return tasks.filter((t) => t.completed === completed);
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description || undefined,
|
||||||
|
completed: row.completed,
|
||||||
|
createdAt: row.created_at.toISOString(),
|
||||||
|
completedAt: row.completed_at ? row.completed_at.toISOString() : undefined,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTask(id: string): Task | undefined {
|
async getTask(id: string): Promise<Task | undefined> {
|
||||||
const tasks = this.readCollection<Task>("tasks");
|
const sql = this.getSql();
|
||||||
return tasks.find((t) => t.id === id);
|
const rows = await sql`
|
||||||
|
SELECT id, title, description, completed, created_at, completed_at
|
||||||
|
FROM tasks
|
||||||
|
WHERE id = ${id}
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const row = rows[0];
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description || undefined,
|
||||||
|
completed: row.completed,
|
||||||
|
createdAt: row.created_at.toISOString(),
|
||||||
|
completedAt: row.completed_at ? row.completed_at.toISOString() : undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Baby Milestones
|
// Baby Milestones
|
||||||
saveBabyMilestone(milestone: BabyMilestone): void {
|
async saveBabyMilestone(milestone: BabyMilestone): Promise<void> {
|
||||||
const milestones = this.readCollection<BabyMilestone>("babyMilestones");
|
const sql = this.getSql();
|
||||||
milestones.push(milestone);
|
await sql`
|
||||||
this.writeCollection("babyMilestones", milestones);
|
INSERT INTO baby_milestones (id, title, description, date, created_at)
|
||||||
|
VALUES (${milestone.id}, ${milestone.title}, ${milestone.description}, ${milestone.date}, ${milestone.createdAt})
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBabyMilestones(): BabyMilestone[] {
|
async getBabyMilestones(): Promise<BabyMilestone[]> {
|
||||||
return this.readCollection<BabyMilestone>("babyMilestones");
|
const sql = this.getSql();
|
||||||
|
const rows = await sql`
|
||||||
|
SELECT id, title, description, date, created_at
|
||||||
|
FROM baby_milestones
|
||||||
|
ORDER BY date DESC
|
||||||
|
`;
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description,
|
||||||
|
date: row.date instanceof Date
|
||||||
|
? row.date.toISOString().split("T")[0]
|
||||||
|
: String(row.date).split("T")[0], // Format as YYYY-MM-DD
|
||||||
|
createdAt: row.created_at instanceof Date
|
||||||
|
? row.created_at.toISOString()
|
||||||
|
: String(row.created_at),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Math Resources
|
// Math Resources
|
||||||
saveMathResource(resource: MathResource): void {
|
async saveMathResource(resource: MathResource): Promise<void> {
|
||||||
const resources = this.readCollection<MathResource>("mathResources");
|
const sql = this.getSql();
|
||||||
const index = resources.findIndex((r) => r.id === resource.id);
|
await sql`
|
||||||
if (index >= 0) {
|
INSERT INTO math_resources (id, title, content, grade, difficulty, tags, created_at)
|
||||||
resources[index] = resource;
|
VALUES (${resource.id}, ${resource.title}, ${resource.content}, ${resource.grade || null}, ${resource.difficulty || null}, ${resource.tags}, ${resource.createdAt})
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
content = EXCLUDED.content,
|
||||||
|
grade = EXCLUDED.grade,
|
||||||
|
difficulty = EXCLUDED.difficulty,
|
||||||
|
tags = EXCLUDED.tags
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMathResources(): Promise<MathResource[]> {
|
||||||
|
const sql = this.getSql();
|
||||||
|
const rows = await sql`
|
||||||
|
SELECT id, title, content, grade, difficulty, tags, created_at
|
||||||
|
FROM math_resources
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
content: row.content,
|
||||||
|
grade: row.grade || undefined,
|
||||||
|
difficulty: row.difficulty || undefined,
|
||||||
|
tags: row.tags || [],
|
||||||
|
createdAt: row.created_at.toISOString(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchMathResources(query: string, grade?: string): Promise<MathResource[]> {
|
||||||
|
const sql = this.getSql();
|
||||||
|
const searchPattern = `%${query}%`;
|
||||||
|
let rows;
|
||||||
|
|
||||||
|
if (grade) {
|
||||||
|
rows = await sql`
|
||||||
|
SELECT id, title, content, grade, difficulty, tags, created_at
|
||||||
|
FROM math_resources
|
||||||
|
WHERE (
|
||||||
|
title ILIKE ${searchPattern} OR
|
||||||
|
content ILIKE ${searchPattern} OR
|
||||||
|
EXISTS (SELECT 1 FROM unnest(tags) AS tag WHERE tag ILIKE ${searchPattern})
|
||||||
|
) AND grade = ${grade}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
resources.push(resource);
|
rows = await sql`
|
||||||
|
SELECT id, title, content, grade, difficulty, tags, created_at
|
||||||
|
FROM math_resources
|
||||||
|
WHERE title ILIKE ${searchPattern} OR content ILIKE ${searchPattern} OR EXISTS (
|
||||||
|
SELECT 1 FROM unnest(tags) AS tag WHERE tag ILIKE ${searchPattern}
|
||||||
|
)
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
this.writeCollection("mathResources", resources);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMathResources(): MathResource[] {
|
return rows.map((row) => ({
|
||||||
return this.readCollection<MathResource>("mathResources");
|
id: row.id,
|
||||||
}
|
title: row.title,
|
||||||
|
content: row.content,
|
||||||
searchMathResources(query: string, grade?: string): MathResource[] {
|
grade: row.grade || undefined,
|
||||||
const resources = this.readCollection<MathResource>("mathResources");
|
difficulty: row.difficulty || undefined,
|
||||||
const lowerQuery = query.toLowerCase();
|
tags: row.tags || [],
|
||||||
return resources.filter((r) => {
|
createdAt: row.created_at.toISOString(),
|
||||||
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
|
// Game Wishlist
|
||||||
saveGameWishlist(game: GameWishlist): void {
|
async saveGameWishlist(game: GameWishlist): Promise<void> {
|
||||||
const games = this.readCollection<GameWishlist>("gameWishlist");
|
const sql = this.getSql();
|
||||||
const index = games.findIndex((g) => g.id === game.id);
|
await sql`
|
||||||
if (index >= 0) {
|
INSERT INTO game_wishlist (id, game_name, platform, notes, added_at)
|
||||||
games[index] = game;
|
VALUES (${game.id}, ${game.gameName}, ${game.platform || null}, ${game.notes || null}, ${game.addedAt})
|
||||||
} else {
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
games.push(game);
|
game_name = EXCLUDED.game_name,
|
||||||
}
|
platform = EXCLUDED.platform,
|
||||||
this.writeCollection("gameWishlist", games);
|
notes = EXCLUDED.notes
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getGameWishlist(): GameWishlist[] {
|
async getGameWishlist(): Promise<GameWishlist[]> {
|
||||||
return this.readCollection<GameWishlist>("gameWishlist");
|
const sql = this.getSql();
|
||||||
|
const rows = await sql`
|
||||||
|
SELECT id, game_name, platform, notes, added_at
|
||||||
|
FROM game_wishlist
|
||||||
|
ORDER BY added_at DESC
|
||||||
|
`;
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
gameName: row.game_name,
|
||||||
|
platform: row.platform || undefined,
|
||||||
|
notes: row.notes || undefined,
|
||||||
|
addedAt: row.added_at.toISOString(),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteGameWishlist(id: string): boolean {
|
async deleteGameWishlist(id: string): Promise<boolean> {
|
||||||
const games = this.readCollection<GameWishlist>("gameWishlist");
|
const sql = this.getSql();
|
||||||
const filtered = games.filter((g) => g.id !== id);
|
const result = await sql`DELETE FROM game_wishlist WHERE id = ${id}`;
|
||||||
if (filtered.length < games.length) {
|
return result.count > 0;
|
||||||
this.writeCollection("gameWishlist", filtered);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
81
src/storage/schema.sql
Normal file
81
src/storage/schema.sql
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
-- Database schema for Cloud MCP Server
|
||||||
|
-- PostgreSQL database schema
|
||||||
|
|
||||||
|
-- Code Snippets table
|
||||||
|
CREATE TABLE IF NOT EXISTS code_snippets (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
language VARCHAR(100) NOT NULL,
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
category VARCHAR(100),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Notes table
|
||||||
|
CREATE TABLE IF NOT EXISTS notes (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tasks table
|
||||||
|
CREATE TABLE IF NOT EXISTS tasks (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Baby Milestones table
|
||||||
|
CREATE TABLE IF NOT EXISTS baby_milestones (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Math Resources table
|
||||||
|
CREATE TABLE IF NOT EXISTS math_resources (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
grade VARCHAR(50),
|
||||||
|
difficulty VARCHAR(50),
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Game Wishlist table
|
||||||
|
CREATE TABLE IF NOT EXISTS game_wishlist (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
game_name TEXT NOT NULL,
|
||||||
|
platform VARCHAR(100),
|
||||||
|
notes TEXT,
|
||||||
|
added_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for better search performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_code_snippets_tags ON code_snippets USING GIN(tags);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_code_snippets_language ON code_snippets(language);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_code_snippets_category ON code_snippets(category);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notes_tags ON notes USING GIN(tags);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tasks_completed ON tasks(completed);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_baby_milestones_date ON baby_milestones(date);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_math_resources_tags ON math_resources USING GIN(tags);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_math_resources_grade ON math_resources(grade);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_math_resources_difficulty ON math_resources(difficulty);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_wishlist_platform ON game_wishlist(platform);
|
||||||
@@ -39,18 +39,19 @@ export function registerNoteTools(): void {
|
|||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
const existingNote = args.id
|
||||||
|
? await database.getNote(args.id as string)
|
||||||
|
: undefined;
|
||||||
const note: Note = {
|
const note: Note = {
|
||||||
id: (args.id as string) || randomUUID(),
|
id: (args.id as string) || randomUUID(),
|
||||||
title: args.title as string,
|
title: args.title as string,
|
||||||
content: args.content as string,
|
content: args.content as string,
|
||||||
tags: (args.tags as string[]) || [],
|
tags: (args.tags as string[]) || [],
|
||||||
createdAt: args.id
|
createdAt: existingNote?.createdAt || now,
|
||||||
? database.getNote(args.id)?.createdAt || now
|
|
||||||
: now,
|
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
database.saveNote(note);
|
await database.saveNote(note);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -81,7 +82,7 @@ export function registerNoteTools(): void {
|
|||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const query = args.query as string;
|
const query = args.query as string;
|
||||||
const notes = database.searchNotes(query);
|
const notes = await database.searchNotes(query);
|
||||||
|
|
||||||
if (notes.length === 0) {
|
if (notes.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -128,7 +129,7 @@ export function registerNoteTools(): void {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const notes = database.getNotes();
|
const notes = await database.getNotes();
|
||||||
const limit = args.limit as number | undefined;
|
const limit = args.limit as number | undefined;
|
||||||
|
|
||||||
// Sort by updated date (newest first)
|
// Sort by updated date (newest first)
|
||||||
@@ -185,7 +186,7 @@ export function registerNoteTools(): void {
|
|||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const id = args.id as string;
|
const id = args.id as string;
|
||||||
const deleted = database.deleteNote(id);
|
const deleted = await database.deleteNote(id);
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function registerTaskTools(): void {
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
database.saveTask(task);
|
await database.saveTask(task);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -67,7 +67,7 @@ export function registerTaskTools(): void {
|
|||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const completed = args.completed as boolean | undefined;
|
const completed = args.completed as boolean | undefined;
|
||||||
const tasks = database.getTasks(completed);
|
const tasks = await database.getTasks(completed);
|
||||||
|
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
const statusText = completed === true ? 'completed' : completed === false ? 'pending' : '';
|
const statusText = completed === true ? 'completed' : completed === false ? 'pending' : '';
|
||||||
@@ -93,9 +93,12 @@ export function registerTaskTools(): void {
|
|||||||
})
|
})
|
||||||
.join('\n\n---\n\n');
|
.join('\n\n---\n\n');
|
||||||
|
|
||||||
const total = database.getTasks().length;
|
const allTasks = await database.getTasks();
|
||||||
const completedCount = database.getTasks(true).length;
|
const completedTasks = await database.getTasks(true);
|
||||||
const pendingCount = database.getTasks(false).length;
|
const pendingTasks = await database.getTasks(false);
|
||||||
|
const total = allTasks.length;
|
||||||
|
const completedCount = completedTasks.length;
|
||||||
|
const pendingCount = pendingTasks.length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -126,7 +129,7 @@ export function registerTaskTools(): void {
|
|||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const id = args.id as string;
|
const id = args.id as string;
|
||||||
const task = database.getTask(id);
|
const task = await database.getTask(id);
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return {
|
return {
|
||||||
@@ -152,7 +155,7 @@ export function registerTaskTools(): void {
|
|||||||
|
|
||||||
task.completed = true;
|
task.completed = true;
|
||||||
task.completedAt = new Date().toISOString();
|
task.completedAt = new Date().toISOString();
|
||||||
database.saveTask(task);
|
await database.saveTask(task);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function registerBabyTools(): void {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
database.saveBabyMilestone(milestone);
|
await database.saveBabyMilestone(milestone);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -71,7 +71,7 @@ export function registerBabyTools(): void {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const milestones = database.getBabyMilestones();
|
const milestones = await database.getBabyMilestones();
|
||||||
const limit = args.limit as number | undefined;
|
const limit = args.limit as number | undefined;
|
||||||
|
|
||||||
// Sort by date (newest first)
|
// Sort by date (newest first)
|
||||||
@@ -157,7 +157,7 @@ export function registerBabyTools(): void {
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
db.saveTask(task);
|
await db.saveTask(task);
|
||||||
|
|
||||||
// Common baby reminders reference
|
// Common baby reminders reference
|
||||||
const commonReminders: Record<string, string> = {
|
const commonReminders: Record<string, string> = {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function registerMathTools(): void {
|
|||||||
const query = args.query as string;
|
const query = args.query as string;
|
||||||
const grade = args.grade as string | undefined;
|
const grade = args.grade as string | undefined;
|
||||||
|
|
||||||
const resources = database.searchMathResources(query, grade);
|
const resources = await database.searchMathResources(query, grade);
|
||||||
|
|
||||||
if (resources.length === 0) {
|
if (resources.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -218,7 +218,7 @@ export function registerMathTools(): void {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
database.saveMathResource(resource);
|
await database.saveMathResource(resource);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export function registerGameTools(): void {
|
|||||||
addedAt: new Date().toISOString(),
|
addedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
database.saveGameWishlist(game);
|
await database.saveGameWishlist(game);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -235,7 +235,7 @@ export function registerGameTools(): void {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
} else if (action === 'list') {
|
} else if (action === 'list') {
|
||||||
const games = database.getGameWishlist();
|
const games = await database.getGameWishlist();
|
||||||
|
|
||||||
if (games.length === 0) {
|
if (games.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -277,7 +277,7 @@ export function registerGameTools(): void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleted = database.deleteGameWishlist(id);
|
const deleted = await database.deleteGameWishlist(id);
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export function registerCodeSnippetTools(): void {
|
|||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
const existingSnippet = args.id
|
||||||
|
? await database.getCodeSnippet(args.id as string)
|
||||||
|
: undefined;
|
||||||
const snippet: CodeSnippet = {
|
const snippet: CodeSnippet = {
|
||||||
id: (args.id as string) || randomUUID(),
|
id: (args.id as string) || randomUUID(),
|
||||||
title: args.title as string,
|
title: args.title as string,
|
||||||
@@ -55,13 +58,11 @@ export function registerCodeSnippetTools(): void {
|
|||||||
language: args.language as string,
|
language: args.language as string,
|
||||||
tags: args.tags as string[],
|
tags: args.tags as string[],
|
||||||
category: args.category as string,
|
category: args.category as string,
|
||||||
createdAt: args.id
|
createdAt: existingSnippet?.createdAt || now,
|
||||||
? database.getCodeSnippet(args.id as string)?.createdAt || now
|
|
||||||
: now,
|
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
database.saveCodeSnippet(snippet);
|
await database.saveCodeSnippet(snippet);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -98,7 +99,7 @@ export function registerCodeSnippetTools(): void {
|
|||||||
async (args) => {
|
async (args) => {
|
||||||
const query = args.query as string;
|
const query = args.query as string;
|
||||||
const tags = args.tags as string[] | undefined;
|
const tags = args.tags as string[] | undefined;
|
||||||
const snippets = database.searchCodeSnippets(query, tags);
|
const snippets = await database.searchCodeSnippets(query, tags);
|
||||||
|
|
||||||
if (snippets.length === 0) {
|
if (snippets.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -149,7 +150,7 @@ export function registerCodeSnippetTools(): void {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const snippets = database.getCodeSnippets();
|
const snippets = await database.getCodeSnippets();
|
||||||
const limit = args.limit as number | undefined;
|
const limit = args.limit as number | undefined;
|
||||||
const limited = limit ? snippets.slice(0, limit) : snippets;
|
const limited = limit ? snippets.slice(0, limit) : snippets;
|
||||||
|
|
||||||
@@ -202,7 +203,7 @@ export function registerCodeSnippetTools(): void {
|
|||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const id = args.id as string;
|
const id = args.id as string;
|
||||||
const deleted = database.deleteCodeSnippet(id);
|
const deleted = await database.deleteCodeSnippet(id);
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,27 +3,88 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { database } from "../../src/storage/database.js";
|
import { database } from "../../src/storage/database.js";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { mkdirSync } from "fs";
|
|
||||||
import type { TestContext } from "./test-utils.js";
|
import type { TestContext } from "./test-utils.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup test database with isolated data directory
|
* Setup test database with isolated database connection
|
||||||
|
* Uses MCP_TEST_DATABASE_URL if provided, otherwise uses DATABASE_URL with a test suffix
|
||||||
*/
|
*/
|
||||||
export function setupTestDatabase(testContext: TestContext): () => void {
|
export async function setupTestDatabase(testContext: TestContext): Promise<() => Promise<void>> {
|
||||||
const testDataDir = join(testContext.tempDir, "data");
|
// Use test database URL if provided, otherwise use main database URL
|
||||||
mkdirSync(testDataDir, { recursive: true });
|
const testDbUrl = process.env.MCP_TEST_DATABASE_URL || process.env.DATABASE_URL;
|
||||||
|
|
||||||
|
if (!testDbUrl) {
|
||||||
|
throw new Error(
|
||||||
|
"MCP_TEST_DATABASE_URL or DATABASE_URL environment variable is required for tests"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Set environment variable for test data directory
|
// Set test database URL
|
||||||
const originalDataDir = process.env.MCP_TEST_DATA_DIR;
|
const originalDbUrl = process.env.DATABASE_URL;
|
||||||
process.env.MCP_TEST_DATA_DIR = testDataDir;
|
process.env.DATABASE_URL = testDbUrl;
|
||||||
|
|
||||||
|
// Initialize database connection
|
||||||
|
await database.initialize();
|
||||||
|
|
||||||
|
// Create tables if they don't exist (using schema.sql)
|
||||||
|
try {
|
||||||
|
const schemaPath = join(process.cwd(), "src", "storage", "schema.sql");
|
||||||
|
const schema = readFileSync(schemaPath, "utf-8");
|
||||||
|
|
||||||
|
// Execute schema (split by semicolons and execute each statement)
|
||||||
|
const statements = schema
|
||||||
|
.split(";")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0 && !s.startsWith("--"));
|
||||||
|
|
||||||
|
// We'll use the database connection directly
|
||||||
|
// Note: This is a simplified approach. In production, you might want to use a migration tool
|
||||||
|
const sql = (database as any).getSql();
|
||||||
|
for (const statement of statements) {
|
||||||
|
if (statement) {
|
||||||
|
try {
|
||||||
|
// Use postgres.unsafe() to execute raw SQL
|
||||||
|
await (sql as any).unsafe(statement);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors for IF NOT EXISTS statements
|
||||||
|
const errorMsg = (error as Error).message;
|
||||||
|
if (!errorMsg.includes("already exists") && !errorMsg.includes("duplicate")) {
|
||||||
|
console.warn(`Schema statement warning: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Could not execute schema.sql:", error);
|
||||||
|
// Continue anyway - tables might already exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up all tables before each test
|
||||||
|
await cleanupTestData();
|
||||||
|
|
||||||
// Return cleanup function
|
// Return cleanup function
|
||||||
return () => {
|
return async () => {
|
||||||
if (originalDataDir) {
|
await cleanupTestData();
|
||||||
process.env.MCP_TEST_DATA_DIR = originalDataDir;
|
await database.close();
|
||||||
|
if (originalDbUrl) {
|
||||||
|
process.env.DATABASE_URL = originalDbUrl;
|
||||||
} else {
|
} else {
|
||||||
delete process.env.MCP_TEST_DATA_DIR;
|
delete process.env.DATABASE_URL;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up test data from all tables
|
||||||
|
*/
|
||||||
|
async function cleanupTestData(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const sql = (database as any).getSql();
|
||||||
|
await sql`TRUNCATE TABLE code_snippets, notes, tasks, baby_milestones, math_resources, game_wishlist RESTART IDENTITY CASCADE`;
|
||||||
|
} catch (error) {
|
||||||
|
// Tables might not exist yet, ignore
|
||||||
|
console.warn("Could not truncate test tables:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user