my-server MCP Server
by vivalalova
- mcp_practice
- src
#!/usr/bin/env node
/**
* 這是一個 MCP 伺服器範本,用於實現一個簡單的筆記系統。
* 它展示了 MCP 的核心概念,如資源和工具,提供以下功能:
* - 列出作為資源的筆記
* - 讀取單個筆記
* - 透過工具創建新筆記
* - 透過提示總結所有筆記
*/
console.log("Starting server...");
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { MongoClient, Collection } from "mongodb";
// MongoDB 連接設定
const MONGODB_URI =
process.env.MONGODB_URI || "mongodb://mongodb.orb.local:27017";
const MONGODB_DB = process.env.MONGODB_DB || "MCP";
const MONGODB_COLLECTION = "notes";
let mongoClient: MongoClient;
let notesCollection: Collection<Note>;
// MongoDB 連接函數
async function connectToMongoDB() {
try {
mongoClient = new MongoClient(MONGODB_URI);
await mongoClient.connect();
const db = mongoClient.db(MONGODB_DB);
notesCollection = db.collection<Note>(MONGODB_COLLECTION);
console.log("Successfully connected to MongoDB");
} catch (error) {
console.error("Failed to connect to MongoDB:", error);
process.exit(1);
}
}
/**
* 定義筆記物件的型別
*/
type Note = {
_id?: string;
title: string;
content: string;
createdAt: Date;
};
/**
* 建立一個 MCP 伺服器,具有資源(用於列出/讀取筆記)、工具(用於創建新筆記)以及提示(用於總結筆記)的功能。
*/
const server = new Server(
{
name: "my-server",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
prompts: {},
},
}
);
/**
* 處理列出可用筆記作為資源的請求。
* 每個筆記以資源的形式呈現,具有:
* - note:// URI 格式
* - 純文字 MIME 類型
* - 具有人性化的名稱和描述(包含筆記標題)
*/
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const notesList = await notesCollection.find({}).toArray();
return {
resources: notesList.map((note) => ({
uri: `note:///${note._id}`,
mimeType: "text/plain",
name: note.title,
description: `A text note: ${note.title}`,
})),
};
});
/**
* 處理讀取特定筆記內容的請求。
* 接收 note:// URI 並以純文字格式返回筆記內容。
*/
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const url = new URL(request.params.uri);
const id = url.pathname.replace(/^\//, "");
const note = await notesCollection.findOne({ _id: id });
if (!note) {
throw new Error(`Note ${id} not found`);
}
return {
contents: [
{
uri: request.params.uri,
mimeType: "text/plain",
text: note.content,
},
],
};
});
/**
* 處理列出可用工具的請求。
* 暴露一個 "create_note" 工具,讓客戶端能夠創建新筆記。
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
// 此請求處理器用於回應列出可用工具的請求
// 我們在此僅暴露一個工具 "create_note",用來讓客戶端創建新筆記
// 此工具定義包含三個部分:
// 1. name:工具的名稱
// 2. description:對工具功能的文字描述
// 3. inputSchema:利用 JSON Schema 規範工具所需的輸入結構,確保客戶端提供的資料中,
// 必須包含 title(筆記標題)和 content(筆記內容)這兩個字串型別的欄位
return {
tools: [
{
name: "create_note", // 工具名稱,代表此功能用於創建新筆記
description: "Create a new note", // 工具描述,解釋功能是用來創建新筆記
inputSchema: {
type: "object", // 輸入資料必須為物件格式
properties: {
title: {
type: "string", // 標題必須是字串
description: "Title of the note", // 標題欄位的描述
},
content: {
type: "string", // 筆記內容必須是字串
description: "Text content of the note", // 筆記內容欄位的描述
},
},
required: ["title", "content"], // 指定 title 與 content 為必填欄位
},
},
],
};
});
/**
* 處理 "create_note" 工具的請求。
* 使用提供的標題和內容創建新筆記,並返回成功訊息。
*/
// 下方代碼是用來處理工具呼叫請求,主要是處理 "create_note" 工具的邏輯。
// 這邊使用 switch...case 結構,可以根據 request 參數中的 name 來決定要執行哪一個工具的邏輯。
// 當收到 "create_note" 的請求,會檢查必填的 title 與 content,接著創建一個新筆記並寫入資料庫,然後返回建立成功的訊息。
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "create_note": {
// 從 request 參數中擷取 title 與 content,並強制轉型為字串
const title = String(request.params.arguments?.title);
const content = String(request.params.arguments?.content);
// 若 title 或 content 為空,則拋出錯誤,告知使用者這兩個欄位皆為必填
if (!title || !content) {
throw new Error("Title and content are required");
}
// 建立一個新的筆記物件,其中包含標題、內容以及建立時間
const newNote = {
title,
content,
createdAt: new Date(), // 使用當前時間記錄筆記的建立時間
};
// 將新的筆記寫入資料庫,並從結果中獲取新筆記的 _id
const result = await notesCollection.insertOne(newNote);
const id = result.insertedId.toString();
// 回傳包含建立成功訊息的回應,訊息中包含新筆記的 id 與標題
return {
content: [
{
type: "text",
text: `Created note ${id}: ${title}`,
},
],
};
}
// 若請求的工具名稱不被支援,則拋出未知工具的錯誤
default:
throw new Error("Unknown tool");
}
});
/**
* 處理列出可用提示的請求。
* 暴露一個 "summarize_notes" 提示,用於總結所有筆記。
*/
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "summarize_notes",
description: "Summarize all notes",
},
],
};
});
/**
* 處理 "summarize_notes" 提示的請求。
* 返回一個提示,要求總結所有筆記,並將筆記內容嵌入為資源。
*/
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
if (request.params.name !== "summarize_notes") {
throw new Error("Unknown prompt");
}
const embeddedNotes = await notesCollection.find({}).toArray();
const notesList = await notesCollection.find({}).toArray();
const embeddedNotesList = notesList.map((note) => ({
type: "resource" as const,
resource: {
uri: `note:///${note._id}`,
mimeType: "text/plain",
text: note.content,
},
}));
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "Please summarize the following notes:",
},
},
...embeddedNotesList.map((note) => ({
role: "user" as const,
content: note,
})),
{
role: "user",
content: {
type: "text",
text: "Provide a concise summary of all the notes above.",
},
},
],
};
});
/**
* 使用 stdio 傳輸啟動伺服器。
* 這使得伺服器可以通過標準輸入/輸出流進行通訊。
*/
async function main() {
await connectToMongoDB();
const transport = new StdioServerTransport();
await server.connect(transport);
}
// 在程式結束時關閉 MongoDB 連接
process.on("SIGINT", async () => {
if (mongoClient) {
await mongoClient.close();
console.log("MongoDB connection closed.");
}
process.exit(0);
});
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});