Skip to main content
Glama
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.