mcp-express-bolierplate
README.md
# MCP Node.js Boilerplate
Boilerplate สำหรับสร้าง MCP server และ Gemini agent ด้วย Node.js + TypeScript โดยฝั่ง HTTP ใช้ Express รองรับทั้ง
- `stdio` — client เปิด server เป็น child process เหมาะกับ MCP host ที่รันในเครื่อง
- Streamable HTTP — endpoint อยู่ที่ `/mcp` และนำออกเป็น HTTPS ได้ด้วย Cloudflare Tunnel
- mock tools สำหรับ CRUD users
- static resource `users://all` และ resource template `users://{id}`
- prompt `summarize-users`
- MCP Inspector สำหรับ discovery, เรียก tool, อ่าน resource และขอ prompt
- Gemini agent ที่รับภาษาธรรมชาติและเลือกเรียก MCP tools ผ่าน OpenAI-compatible API
ข้อมูลเริ่มต้นอยู่ที่ `src/data/users.json` และถูกโหลดเข้า memory เมื่อเปิด server การแก้ไขผ่าน CRUD จะไม่เขียนทับไฟล์ และจะ reset เมื่อ restart process
## Requirements
- Node.js 22.19 ขึ้นไป
- npm
- `cloudflared` เฉพาะกรณีต้องการ HTTPS tunnel
- Gemini API key เฉพาะกรณีรัน Agent client
## ติดตั้ง
```bash
npm install
```
ตรวจ build และ test:
```bash
npm run check
```
## โครงสร้างสำคัญ
```text
src/
├── agent/
│ ├── agent.ts # Gemini tool-calling loop สำหรับภาษาธรรมชาติ
│ └── mcp-transport.ts # MCP transport สำหรับ Agent
├── data/
│ └── users.json # mock seed data
├── lib/
│ └── api-client.ts # shared Axios instance สำหรับ upstream APIs
├── services/
│ └── user-service.ts # business logic กลางสำหรับ MCP capabilities
└── server/
├── mcp.ts # ประกอบ server และ capability registrations
├── tools/
│ └── user-tools.ts
├── resources/
│ └── user-resources.ts
├── prompts/
│ └── user-prompts.ts
├── schemas/
│ └── user.ts # shared MCP output schema
├── repository.ts # in-memory CRUD repository
├── stdio.ts # stdio entry point
└── http.ts # Express + Streamable HTTP entry point
scripts/
└── build.mjs # compile TypeScript และ copy mock JSON ไป dist
```
Factory ใน `mcp.ts` ถูกใช้ร่วมกันทั้งสอง transport ทำให้ความสามารถของ server ไม่ต่างกัน โดย Tools, Resources และ Prompts เรียก `UserService` กลางแทนการผูกกับ repository โดยตรง
## เรียก External API ด้วย Axios
โปรเจกต์มี shared Axios instance ที่ `src/lib/api-client.ts` พร้อม base URL, timeout และ optional Bearer token สามารถ import ไปใช้ใน tool หรือ service ได้:
```ts
import { apiClient } from "../../lib/api-client.js";
const response = await apiClient.get("/users");
console.log(response.data);
```
กำหนดค่าตอนเปิด server:
```bash
API_BASE_URL=https://api.example.com \
API_TIMEOUT_MS=10000 \
API_TOKEN=your-token \
npm run server:http
```
ตัวอย่างนำไปใช้ใน MCP tool:
```ts
server.registerTool(
"list-upstream-users",
{
description: "List users from the configured upstream API",
inputSchema: z.object({}),
},
async () => {
const { data } = await apiClient.get("/users");
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
structuredContent: { users: data },
};
},
);
```
หากไม่กำหนด `API_BASE_URL` ยังสามารถส่ง absolute URL ให้ Axios ได้โดยตรง หลีกเลี่ยงการ log `API_TOKEN` และควรเก็บ token ใน secret manager เมื่อ deploy production
## วิธีรันแบบ stdio
ปกติไม่ต้องเปิด stdio server แยก เพราะ Inspector, Agent หรือ MCP host จะ spawn process ให้เอง
เปิด MCP Inspector พร้อม stdio server:
```bash
npm run inspector:stdio
```
เปิด server ตรง ๆ เพื่อรอ MCP host:
```bash
npm run server:stdio
```
ข้อควรระวัง: stdio ใช้ `stdout` เป็นช่อง JSON-RPC ดังนั้น log ของ server ต้องเขียนผ่าน `stderr` เช่น `console.error` เท่านั้น
ตัวอย่าง config สำหรับ MCP host โดยเปลี่ยน `/absolute/path/to/mcp-boilerplate` เป็น path จริง:
```json
{
"mcpServers": {
"mock-users": {
"command": "node",
"args": [
"--import",
"tsx",
"/absolute/path/to/mcp-boilerplate/src/server/stdio.ts"
],
"cwd": "/absolute/path/to/mcp-boilerplate"
}
}
}
```
หรือ build ก่อนแล้วใช้ JavaScript โดยไม่ต้องพึ่ง `tsx` ตอน runtime:
```bash
npm run build
npm run start:stdio
```
config หลัง build:
```json
{
"mcpServers": {
"mock-users": {
"command": "node",
"args": [
"/absolute/path/to/mcp-boilerplate/dist/server/stdio.js"
],
"cwd": "/absolute/path/to/mcp-boilerplate"
}
}
}
```
## วิธีรันแบบ Express HTTP
Terminal 1 — เปิด server:
```bash
npm run server:http
```
ค่า default:
- MCP endpoint: `http://127.0.0.1:3000/mcp`
- health check: `http://127.0.0.1:3000/health`
Terminal 2 — เปิด MCP Inspector และเชื่อมต่อ `/mcp`:
```bash
npm run inspector:http
```
เปลี่ยน port หรือ host ได้ด้วย environment variables:
```bash
HOST=127.0.0.1 PORT=4000 npm run server:http
npx @modelcontextprotocol/inspector --server-url http://127.0.0.1:4000/mcp --transport http
```
สำหรับ production build:
```bash
npm run build
npm run start:http
```
## เปิด HTTPS ด้วย Cloudflare Tunnel
HTTPS ในตัวอย่างนี้ terminate ที่ Cloudflare ส่วน Express server ยังฟัง HTTP เฉพาะในเครื่อง
macOS ติดตั้ง `cloudflared`:
```bash
brew install cloudflared
```
Terminal 1 — เปิด MCP HTTP server:
```bash
npm run server:http
```
Terminal 2 — เปิด Quick Tunnel:
```bash
cloudflared tunnel --url http://127.0.0.1:3000
```
`cloudflared` จะแสดง URL ชั่วคราว เช่น:
```text
https://random-words.trycloudflare.com
```
MCP endpoint ภายนอกจึงเป็น:
```text
https://random-words.trycloudflare.com/mcp
```
Terminal 3 — ทดสอบผ่าน HTTPS tunnel:
```bash
npx @modelcontextprotocol/inspector --server-url https://random-words.trycloudflare.com/mcp --transport http
```
Quick Tunnel เหมาะสำหรับ development เท่านั้น และ Cloudflare ระบุว่าไม่รองรับ SSE ดังนั้น boilerplate นี้ตั้ง response mode เป็น `auto` ซึ่งคำสั่ง CRUD/discovery ทั่วไปจะตอบ JSON ได้ แต่ไม่ควรใช้ Quick Tunnel ทดสอบฟีเจอร์ที่ต้อง stream เช่น subscription ระยะยาว สำหรับ production ให้ใช้ named tunnel, hostname ของตนเอง, authentication และ authorization
เมื่อใช้ custom hostname ให้เพิ่ม hostname ใน allowlist:
```bash
ALLOWED_HOSTS=mcp.example.com npm run server:http
```
หลาย hostname คั่นด้วย comma:
```bash
ALLOWED_HOSTS=mcp.example.com,mcp-staging.example.com npm run server:http
```
`localhost`, `127.0.0.1`, `::1` และ `*.trycloudflare.com` ถูกอนุญาตไว้สำหรับ development แล้ว
## MCP Inspector
Inspector เป็นเครื่องมือหลักสำหรับตรวจ Server โดยไม่ผ่านโมเดล ใช้ดูและเรียก Tools, Resources และ Prompts ผ่าน Web UI
stdio — Inspector จะ spawn server ให้:
```bash
npm run inspector:stdio
```
HTTP — เปิด `npm run server:http` ก่อน แล้วรัน:
```bash
npm run inspector:http
```
สำหรับ remote URL:
```bash
npx @modelcontextprotocol/inspector --server-url https://mcp.example.com/mcp --transport http
```
Inspector v2 ต้องใช้ Node.js 22.19 ขึ้นไป ตัว server และ Agent จึงกำหนด Node.js requirement เดียวกันเพื่อลดความต่างระหว่าง development กับ production
## Gemini MCP Agent
`agent.ts` มี MCP Client อยู่ภายในเพื่อเชื่อม Server, ส่ง MCP tool schemas ให้ Gemini, รัน tool calls และส่งผลกลับให้โมเดลจนได้คำตอบสุดท้าย
```text
User prompt → Gemini → MCP tool call → MCP Server → tool result → Gemini → answer
```
สร้างไฟล์ `.env` และใส่ API key:
```bash
cp .env.example .env
```
```env
GEMINI_API_KEY=your_real_key
GEMINI_MODEL=gemini-3.7-flash
```
รัน interactive agent ผ่าน stdio โดยไม่ต้องเปิด server แยก:
```bash
npm run agent:stdio
```
หรือส่งคำถามครั้งเดียว:
```bash
npm run agent:stdio -- "แสดงผู้ใช้ทั้งหมด"
npm run agent:stdio -- "สร้างผู้ใช้ชื่อ John อีเมล john@example.com role developer"
```
สำหรับ HTTP ให้เปิด server ใน Terminal 1:
```bash
npm run server:http
```
แล้วเปิด Agent ใน Terminal 2:
```bash
npm run agent:http
```
หรือ one-shot:
```bash
npm run agent:http -- "ดูรายละเอียด user ID 1"
```
Agent ใช้ OpenAI SDK กับ Gemini OpenAI-compatible endpoint ค่า `GEMINI_BASE_URL` จึงสามารถเปลี่ยนได้หากต้องการใช้ compatible gateway อื่น แต่ tool-calling compatibility ของแต่ละ provider อาจไม่เหมือนกันทั้งหมด
### Model compatibility
Agent ใน boilerplate นี้ยังไม่ได้ model-agnostic 100% โดยผูกกับ OpenAI-compatible Chat Completions API, โครงสร้าง `tool_calls` และ OpenAI SDK แต่ไม่ได้ผูกกับ Gemini SDK โดยตรง
- **Gemini:** ใช้ได้ทันทีผ่าน Gemini OpenAI-compatible endpoint ตามค่า default
- **OpenAI:** ใช้ได้โดยตั้ง `GEMINI_BASE_URL` เป็น OpenAI API base URL และกำหนด API key/model ของ OpenAI ในตัวแปรเดิม
- **Anthropic native API:** ยังใช้โดยตรงไม่ได้ เพราะ message และ tool-use schema ต่างจาก OpenAI-compatible API ต้องเพิ่ม Anthropic adapter หรือใช้ gateway ที่แปลงเป็น OpenAI-compatible API
ตัวแปรยังใช้ prefix `GEMINI_` เพราะ Gemini เป็น provider ตัวอย่างของ boilerplate นี้ ตัวอย่างการชี้ไป OpenAI:
```env
GEMINI_API_KEY=your_openai_api_key
GEMINI_MODEL=your_openai_model
GEMINI_BASE_URL=https://api.openai.com/v1/
```
หากต้องการรองรับหลาย provider ใน production ควรแยก interface เช่น `ModelProvider` แล้วสร้าง adapter สำหรับ Gemini/OpenAI/Anthropic โดยให้ MCP client และ tool execution loop ใช้ interface กลางร่วมกัน
## Tools, resources และ prompt ที่มีให้
| ชนิด | ชื่อ | หน้าที่ |
| --- | --- | --- |
| Tool | `list-users` | ดู users ทั้งหมด |
| Tool | `get-user` | ดู user ตาม ID |
| Tool | `create-user` | สร้าง user |
| Tool | `update-user` | แก้ไข user |
| Tool | `delete-user` | ลบ user |
| Resource | `users://all` | JSON snapshot ของ users ทั้งหมด |
| Resource template | `users://{id}` | JSON ของ user รายคน พร้อม ID completion |
| Prompt | `summarize-users` | สร้างข้อความให้โมเดลสรุปข้อมูล users |
## Environment variables
| ตัวแปร | Default | ใช้กับ |
| --- | --- | --- |
| `HOST` | `127.0.0.1` | Express server bind address |
| `PORT` | `3000` | Express server port |
| `MCP_URL` | `http://127.0.0.1:3000/mcp` | HTTP endpoint ที่ Agent เชื่อมต่อ |
| `MCP_ACCESS_TOKEN` | ไม่กำหนด | Bearer token สำหรับ remote MCP server |
| `ALLOWED_HOSTS` | ว่าง | เพิ่ม custom Host/Origin ที่ server ยอมรับ |
| `GEMINI_API_KEY` | จำเป็นสำหรับ Agent | Gemini API key |
| `GEMINI_MODEL` | `gemini-3.7-flash` | โมเดลที่ Agent ใช้ |
| `GEMINI_BASE_URL` | Gemini OpenAI-compatible URL | Model API endpoint |
| `AGENT_MAX_TOOL_STEPS` | `10` | จำนวน tool-call rounds สูงสุดต่อคำถาม |
| `API_BASE_URL` | ไม่กำหนด | Base URL ของ upstream API ที่ Axios เรียก |
| `API_TIMEOUT_MS` | `10000` | Axios request timeout หน่วยมิลลิวินาที |
| `API_TOKEN` | ไม่กำหนด | Bearer token ที่ Axios แนบให้อัตโนมัติ |
ตัวอย่างค่าอยู่ใน `.env.example` โดย Agent จะโหลด `.env` อัตโนมัติผ่าน `dotenv` ส่วน Server และ Inspector ให้ export ตัวแปรหรือใส่ไว้หน้าคำสั่งตามตัวอย่างด้านบน
## Security notes
- ตัวอย่างนี้ยังไม่มี authentication และ authorization ห้ามเปิด public endpoint ที่มีข้อมูลจริง
- validation ของ `Host` และ `Origin` เปิดเฉพาะ localhost, TryCloudflare และค่าจาก `ALLOWED_HOSTS`
- mock repository อยู่ใน memory และตั้งใจไม่ persist ข้อมูล
- สำหรับ production ควรเพิ่ม auth, rate limiting, audit logging, persistent database และ TLS/trust-proxy configuration ที่เหมาะกับระบบจริง
## Scripts ทั้งหมด
```bash
npm run dev:stdio # stdio server พร้อม watch mode
npm run dev:http # Express HTTP server พร้อม watch mode
npm run server:stdio # stdio server จาก TypeScript
npm run server:http # Express HTTP server จาก TypeScript
npm run agent:stdio
npm run agent:http
npm run inspector:stdio
npm run inspector:http # ต้องเปิด server:http ก่อน
npm run build
npm run start:stdio # รัน dist หลัง build
npm run start:http # รัน dist หลัง build
npm run start:agent:stdio
npm run start:agent:http
npm test
npm run check
```
อ้างอิง: [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk), [MCP Inspector](https://github.com/modelcontextprotocol/inspector), [Gemini OpenAI compatibility](https://ai.google.dev/gemini-api/docs/openai), [OpenAI Chat Completions](https://developers.openai.com/api/reference/cli/resources/chat/subresources/completions), [Cloudflare Quick Tunnels](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues