實作一個簡易 MCP Server - 以 TypeScript 實作
tags: TypeScript
MCP
category: MCP
description: 實作一個簡易 MCP Server - 以 TypeScript 實作
created_at: 2025/04/05 14:00:00
事前準備
- 準備好
Node.js
環境 - 有
MCP
的Client
可以連線測試
前言
前陣子 MCP
等其他 AI
相關工具開始流行起來,本來想說繼續讓子彈飛一會(?),但後來一直被推播各種資訊(即使我平常都用無痕也受到影響),然後再加上這陣子開始撰寫碩士論文了,如果只將這些 AI
工具放在未來展望又不自己玩玩看好像有點可惜(而且說不定玩了之後會有更多想法),所以就決定來寫一個簡易的 MCP Server
。
相信大家對 MCP
已經聽到爛了,所以就不多介紹,直接來實作簡易的 MCP Server
來玩,絕對不是我懶得介紹
實作
這邊打算以 TypeScript
來實作,當然你也可以到 Github 上看看有哪些 sdk
可以用,這樣會簡單很多。
當然上面也有大量現成的 MCP Server
可以用,但既然這篇是要自己做,所以就先不考慮。
首先是 IDE
的選擇,這邊我先採用 Visual Studio Code Insiders
,這和原本的 Visual Studio Code
沒有太大差別,主要是因為 Insiders
版本會比 Stable
版本更新一些,也許未來 VSCode
也會原生支援 MCP
,所以就先用 Insiders
版本來寫。
準備好了 IDE
之後,接下來假設你有 Node
環境的話,先建立一個資料夾,然後在裡面初始化專案:
接下來我都假設以原生 npm
當範例,但是當然也可以用其他你熟悉的套件管理器,例如 yarn
或 pnpm
甚至 bun
等等。
npm init -y
建立好專案之後,我們先安裝一些必要的東西,這邊我們需要安裝 typescript
、ts-up
,還有 MCP
需要的東西 @modelcontextprotocol/sdk
還有他其實有用到但沒寫在文件上的 。zod
npm install typescript tsup @modelcontextprotocol/sdk zod
接下來去修改 package.json
裡面的 scripts
,加入 build
指令,用來編譯 TypeScript
(假設我們檔案放在 src
資料夾裡面,然後檔名是 server.ts
)。
"scripts": {
"build": "tsup src/server.ts"
},
接著我們就建立 src/server.ts
,然後貼上官方給的範例程式碼感受一下:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Create an MCP server
const server = new McpServer({
name: "Demo",
version: "1.0.0"
});
// Add an addition tool
server.tool("add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// Add a dynamic greeting resource
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
然後嘗試去 build
看看,你應該會看到一個錯誤。
ERROR: Top-level await is currently not supported with the "cjs" output format
這是因為 tsup
預設會使用 cjs
的格式來編譯,然後 cjs
格式不支援頂層 await
,既然知道原因之後,基本上你有幾種常見的解法:
- 在
tsup
的package.json
裡面加上--format esm
,這樣就會用esm
格式來編譯了。 - 不要用頂層
await
,反正他後面也沒做什麼事。
這邊為了簡單,乾脆直接選 2
,所以把 await
拿掉就好,然後為了避免你覺得他沒在動,所以在最後加上一行 console.log
,所以會變成這樣:
server.connect(transport);
console.log("Server started, waiting for messages...");
這時候你再去編譯一次,應該就不會有問題了。
當產生好我們的 dist/server.js
之後,我們就可以開始測試了,這邊我們可以用 node
來執行看看,至少先確定沒有錯誤:
node dist/server.js
如果沒有錯誤的話,這時候你應該會看到 Server started, waiting for messages...
的訊息,這樣就代表我們的 MCP Server
已經啟動了,接著把它終止(Ctrl+C
)掉,再來我們要用 Insiders
來測試看看。
如果對 VSCode
熟悉的話,應該都知道在上方有一個 Chat
的功能,在 Insiders
裡面也是一樣,只是目前 Insiders
右下方還可以選擇模式,這邊我們選擇 Agent
模式,
切換過去之後,左邊會再看到一個板手的圖示,這邊可以設定你的 MCP Server
按下去之後,會有一系列的互動,首先選擇 Add MCP Server...
接著選擇 Command (stdio)
然後他會問你要怎麼啟動這個 MCP Server
對我們來說,就是 node dist/server.js
,然後按下 Enter
鍵之後,他會問你要不要給這個 MCP Server
一個名字,這邊隨便取一個就好(他也會幫你產生一個預設隨機的名字),然後按下 Enter
鍵之後。
他會問你要將這個設定檔放置在哪裡,分別有 User
和 Workspace
兩個選項,這邊我們選擇 Workspace
,然後按下 Enter
鍵之後。
這兩個的差異主要是 User
的話任何專案都會套用,而 Workspace
的話只有這個專案會套用,而既然只是實驗的話,將影響鎖在最小的範圍可能是比較好的選擇。
接著他就會產生一個 mcp.json
,裡面就是你的 mcp server
的設定檔,然後你會看到裡面有個 Error
你可以將這個 Error
按下去,然後看看下方的錯誤訊息,你會發現因為他執行的時候會從你的 User
根目錄下去找,所以會找不到你的模組(Error: Cannot find module
),所以這時候你有幾種選擇:
- 使用絕對路徑
- 使用
npx
來執行
這裡你可以簡單起見,先使用絕對路徑來玩玩,之後如果需要移植的時候也許 npx
是比較好的選擇。
所以接著你需要把 args
當中的路徑改成絕對路徑,然後記得存檔,然後點下 Restart
,之後應該就會正常啟動你的 MCP Server
。
然後你可以回去看看右邊 Chat
當中那個板手的 Icon
,應該會看到一個 1
,代表有一個 Tool
可以使用。
這時你就可以嘗試和他聊天,比方說問他 123+456
第一次使用可能會以為,太棒了一切正常,但是其實他沒有呼叫我們的 MCP Server
,而是直接用 Chat
的內建功能來計算的,不相信的話
所以我先明確告訴他說用我的,然後你也會發現到,若有使用你提供的方法的話,他會問你要不要繼續
你也可以展開來看他送什麼樣的參數進去給你的工具,但這邊我就先不展開了,然後你可以按下 Continue
,這樣就會繼續執行了。
如果你還是懷疑他沒有用你的 MCP Server
的話,我們來改一下你的程式碼:
// Add an addition tool
server.tool("add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b * 2) }]
})
);
然後重新編譯,重新編譯之後記得去重啟 MCP Server
,然後再試一次加法,這次我們問他 1+2=?
有時他會回應你說計算結果可能有誤,因為 AI
認為你的計算結果怪怪的,提醒你一下,就像是下面這樣
但總之他有照著你的程式去跑,到這邊,基本的 MCP Server
就完成了。
其他應用
簡單講就有點像是你用自然語言去問 AI
,然後 AI
會根據你說的話去決定要執行哪一些函數,然後回傳結果給你,這樣的話就可以讓 AI
來幫你做一些事情了,這種事情很多,總之發揮想像力(?)
換句話說如果有一些平台假設後台做得很爛,如果整 MCP
整的好,也許可以拉回使用者體驗(?),反正跟 AI
說你要幹嘛,然後後面瑣碎的事情由他幫你完成,比方說我假設以部落格當例子,透過這個例子,應該就可以聯想到其他應用(?)
比方說我準備兩個方法,一個是建立文章,一個是取得文章內容。 (這個範例可能沒什麼意義,但是只是為了舉例 AI
幫你做事(?))
server.tool("new post",
{ title: z.string(), content: z.string() },
async ({ title, content }) => ({
content: [{ type: "text", text: `New post created with title: ${title}` }]
})
);
server.tool('get post content',
{ id: z.string() },
async ({ id }) => ({
content: [{ type: "text", text: `Content of post ${id}` }]
})
);
然後去問他:
然後他就會回你上面寫死的內容,實際應用你可以真的去串接資料,然後再回應出去。
當然也可以一次問多篇,例如:
當知道他可以一次幫你做很多件事之後,一切就可以開始變得有趣起來了(?),假設我讓他幫我以前三篇文章的內容做總結然後幫我寫一篇新的文章:
大概上就是這樣,發揮想像力的話應該可以做出更多有趣的東西。
結論
這篇主要就是一個手動寫一個簡易 MCP Server
的過程,有一些小細節其實可以提一下,就是如果你用 Bun
的話,其實你可以不用安裝 typescript
,因為 Bun
內建支援 TypeScript
,換句話說你可以直接用 Bun
來執行 TypeScript
,這樣就不需要安裝 typescript
和 tsup
了。
如果你都使用 Bun
的話,這點小細節我可能也不太需要提醒就是XD
然後其實第一時間我覺得 MCP Server
目前在做的事其實很像是在我大學期間玩 Chatbot
的時候就有工具在做類似的事情了(我印象中是 Google
提供的工具,但是哪一套名字我目前想不起來),就是根據使用者的訊息去做一些分析,接著去自動決定要觸發哪一些 handler
或者是說 callback
然後我們就可以做相應的處理,只是沒想到現在有個 MCP
把這些概念都整合起來了。