Skip to main content
Glama
Pongsapat1035

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/)