Simple-MCP-Server
by kindinh903
README.md
# MCP Agent Homework
A TypeScript MCP (Model Context Protocol) system built for the assignment in
[MCP_HOMEWORK_SKILL.md](MCP_HOMEWORK_SKILL.md): an **Agent Host** that loads an
**Agent Skill** ([SKILL.md](SKILL.md)), connects to three MCP servers over all
three required transports, discovers/aggregates their tools, and lets **Gemini**
select and call the right tool on the right server.
## Architecture
```text
Agent Host (src/host)
skill-loader + connection-manager
+ tool-bridge + gemini-client
|
+------------------+------------------+
| | |
v v v
stdio server local HTTP server public HTTP server
(src/servers/stdio- (src/servers/http- (same http-server.ts,
server.ts) server.ts, no auth) API-key protected)
| | |
+------------------+-------------------+
|
shared tool logic (src/servers/shared/tools.ts)
3 tools (calculator, text_stats, unit_convert) + 1 resource + 1 prompt
```
- **`src/servers/shared/tools.ts`** — the single implementation of the 3 tools,
1 resource, and 1 prompt, registered identically on every server so the same
logic is reused everywhere (no duplicated business logic).
- **`src/servers/stdio-server.ts`** — MCP over stdio (spawned as a child process).
- **`src/servers/http-server.ts`** — MCP over Streamable HTTP. The exact same
file/code runs both the "local" and "public" servers; the only difference is
configuration (`PORT`, `PUBLIC_MCP_API_KEY`).
- **`src/host/connection-manager.ts`** — the MCP Host: connects to every
configured server, discovers tools/resources/prompts, namespaces tool names
as `<namespace>__<tool>` to avoid collisions, and dispatches tool calls back
to the owning server.
- **`src/host/tool-bridge.ts`** — converts discovered MCP tools into Gemini
function declarations.
- **`src/host/gemini-client.ts`** — the Gemini tool-calling loop (send message →
read function calls → dispatch via connection manager → send function
responses back → repeat until final text).
- **`src/host/skill-loader.ts`** — loads [SKILL.md](SKILL.md) and injects it as
the model's system instruction, so the skill actively shapes tool usage.
- **`src/host/agent-host.ts`** — wires the above together from
[config/servers.json](config/servers.json).
- **`src/host/cli.ts`** — CLI entry point (interactive or `--demo`).
## Setup
```powershell
npm install
```
Secrets live in `api.env` (already gitignored):
```env
API_KEY=your-gemini-api-key
# Optional, only needed once you deploy the public server:
# PUBLIC_MCP_URL=https://your-app.onrender.com/mcp
# PUBLIC_MCP_API_KEY=some-strong-random-key
```
## Running each component
### stdio server (20 pts)
```powershell
npm run server:stdio # run directly
npm run inspector:stdio # open MCP Inspector against it
```
Inspector will discover **3 tools** (`calculator`, `text_stats`,
`unit_convert`), **1 resource** (`docs://unit-conversions`), and **1 prompt**
(`explain-tool-result`), and can execute/read all of them.
### Local HTTP server
```powershell
npm run server:http # listens on http://127.0.0.1:8787/mcp, no auth
npm run inspector:http # then connect Inspector to that URL
```
### Public HTTP server (15 pts)
The same `http-server.ts` becomes the "public" server once
`PUBLIC_MCP_API_KEY` is set — every request then requires a matching
`x-api-key` header; missing/invalid keys get `401 Unauthorized`.
```powershell
$env:PORT=8788; $env:PUBLIC_MCP_API_KEY="a-strong-secret"; npm run server:http
```
Deploying it publicly (Render.com, using the included [render.yaml](render.yaml)):
1. `git init && git add -A && git commit -m "MCP homework"` then push to a
GitHub repo you own.
2. In Render: **New +** → **Blueprint** → select the repo (it reads
`render.yaml` automatically), or create a **Web Service** manually with:
- Build command: `npm install && npm run build`
- Start command: `npm run start:http`
- Health check path: `/health`
3. In the Render dashboard, set the `PUBLIC_MCP_API_KEY` environment variable
to a strong secret (never commit it).
4. Once deployed, put the resulting URL + key into `api.env`:
`PUBLIC_MCP_URL=https://<your-service>.onrender.com/mcp` and
`PUBLIC_MCP_API_KEY=<same secret>`.
5. Validate with Inspector:
- No key → rejected: `curl -X POST https://<url>/mcp -H "Content-Type: application/json" -d "{...}"` returns `401`.
- With key → works: pass `--header "x-api-key: <secret>"` to
`npx @modelcontextprotocol/inspector --cli <url> --method tools/list`.
### Agent Host
```powershell
npm run agent # interactive CLI
npm run agent:demo # runs a scripted set of demo queries
```
On startup the host:
1. Loads `SKILL.md` as the system instruction.
2. Reads [config/servers.json](config/servers.json) and connects to the stdio
server (spawned automatically), the local HTTP server (must already be
running), and the public HTTP server (skipped automatically if
`PUBLIC_MCP_URL`/`PUBLIC_MCP_API_KEY` aren't set — it's optional so the demo
still works without a live deployment).
3. Discovers and namespaces every tool, hands them to Gemini, and dispatches
each tool call Gemini makes to the correct MCP server.
## Configuration
Server registration is data-driven via [config/servers.json](config/servers.json)
— add/remove servers there instead of editing host code. `${VAR}` in a `url`
is resolved from `process.env` at connect time; `apiKeyEnv` names the env var
whose value is sent as `x-api-key`.
## Agent Skill
[SKILL.md](SKILL.md) instructs the agent to prefer calling tools over
guessing at arithmetic/conversions/text stats, to pick one namespaced tool per
logical request, to consult the `docs://unit-conversions` resource when unsure
about supported conversions, and to explain results in plain language. It is
loaded verbatim into the Gemini system instruction on every run (see
`src/host/skill-loader.ts`), so its rules directly affect tool selection and
response style — observable in the demo output (e.g. the agent always calls a
tool for arithmetic instead of computing it itself).
## Security notes
- No secrets are committed; `api.env` is gitignored and the public server only
reads `PUBLIC_MCP_API_KEY` from the environment.
- The public HTTP server rejects any request without a matching `x-api-key`
header (401), and accepts requests once a valid key is supplied.