Skip to main content
Glama
MichaelGNielsen

dad-jokes-mcp

README.md
> **Agent instructions:** See [`AGENTS.md`](AGENTS.md) for architecture, commands, and how to add tools.
>
> **Note:** Denne MCP server er fuldt kompatibel med MCP-klienter der understøtter HTTP transport, herunder **Page Assist**, **Gemini**, og andre moderne AI-værktøjer.

## Available Tools

| Tool Name | Description | Input | Output | Example | curl |
|-----------|-------------|-------|--------|----------|------|
| `get_random_joke` | Fetch a random dad joke and save it | None | Joke text | `@dad-jokes-mcp get_random_joke` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_random_joke","arguments":{}}}'` |
| `get_multiple_jokes` | Fetch multiple random dad jokes at once | `count` (1-20, default: 5) | Array of jokes | `@dad-jokes-mcp get_multiple_jokes count=10` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_multiple_jokes","arguments":{"count":10}}}'` |
| `server_status` | Show server version, sources count, stored jokes, tools | None | JSON status | `@dad-jokes-mcp server_status` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"server_status","arguments":{}}}'` |
| `get_all_jokes` | View all jokes stored in www/jokes.json | None | All saved jokes | `@dad-jokes-mcp get_all_jokes` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_all_jokes","arguments":{}}}'` |
| `clear_jokes` | Delete all saved jokes | None | Confirmation | `@dad-jokes-mcp clear_jokes` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"clear_jokes","arguments":{}}}'` |
| `get_joke_category` | Get joke from category | `category` (enum) | Joke from category | `@dad-jokes-mcp get_joke_category category=Programming` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_joke_category","arguments":{"category":"Programming"}}}'` |
| `fill_jokes_batch` | Ensure at least N jokes are stored; fetches if needed | `count` (1-20, default: 5) | N jokes from pool | `@dad-jokes-mcp fill_jokes_batch count=10` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"fill_jokes_batch","arguments":{"count":10}}}'` |
| `add_jokes` | Fetch and store N new jokes unconditionally | `count` (1-20, default: 5) | N new jokes added | `@dad-jokes-mcp add_jokes count=10` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add_jokes","arguments":{"count":10}}}'` |
| `add_joke` | Add a custom joke manually | `text` (string) | Confirmation | `@dad-jokes-mcp add_joke text="Why..."` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add_joke","arguments":{"text":"Why did the chicken cross the road?"}}}'` |
| `clean_jokes` | Remove null/empty entries from the pool | None | Cleanup result | `@dad-jokes-mcp clean_jokes` | `curl -X POST http://localhost:5000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"clean_jokes","arguments":{}}}'` |

## How MCP Tool Routing Works

### The Short Answer

Nej, det er **ikke hardkodet**. Det er **dynamisk routing** baseret på tool-navn.

### Hvad der sker når du skriver:

```
@dad-jokes-mcp get_multiple_jokes count=10
```

**Step 1: PA parser din besked**
- PA ser `@dad-jokes-mcp` = server navn
- PA ser `get_multiple_jokes` = tool navn
- PA ser `count=10` = argument

**Step 2: PA laver en JSON-RPC request**

```json
{
  "jsonrpc": "2.0",
  "id": 123,
  "method": "tools/call",
  "params": {
    "name": "get_multiple_jokes",
    "arguments": {
      "count": 10
    }
  }
}
```

**Step 3: MCP Server modtager request**

Serveren (dad_jokes_mcp.mjs) modtager JSON via POST `/mcp`:

```javascript
if (request.method === "tools/call") {
  const { name, arguments: args } = request.params;

  // name = "get_multiple_jokes"
  // args = { count: 10 }


**Step 4: Server matcher tool-navn og udfører**

Serveren bruger et **handler lookup-objekt** i stedet for if/else:

```javascript
const handlers = {
  get_random_joke: async () => { /* ... */ },
  get_multiple_jokes: async () => { /* ... */ },  // ← MATCH!
  get_all_jokes: async () => { /* ... */ },
  clear_jokes: async () => { /* ... */ },
  get_joke_category: async () => { /* ... */ },
  fill_jokes_batch: async () => { /* ... */ },
  add_jokes: async () => { /* ... */ },
  add_joke: async () => { /* ... */ },
  clean_jokes: async () => { /* ... */ },
};

const handler = handlers[name];
if (handler) {
  toolResponse = await handler(args);
} else {
  toolResponse = { error: { code: -32601, message: `Tool not found: ${name}` } };
}
```

**Step 5: Server returnerer resultat**

```json
{
  "jsonrpc": "2.0",
  "id": 123,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "😄 10 Dad Jokes:\n\n1. ...\n2. ..."
      }
    ]
  }
}
```

**Step 6: PA viser resultatet**

PA modtager resultatet og viser det for brugeren.

### Hvorfor er det ikke hardkodet?

1. **Tool-navne er defineret dynamisk** - I `tools/list` metoden returnerer serveren hvilke tools der findes
2. **Arguments er dynamiske** - Hver tool kan have forskellige inputs
3. **Routing er baseret på string matching** - `if (name === "tool_name")` matcher kun hvis navn passer

### Hvis du tilføjede en ny tool

Du skulle:

1. Tilføje den til `tools/list` response:
```javascript
{
  name: "my_new_tool",
  description: "What it does",
  inputSchema: { /* ... */ }
}
```

2. Tilføje en key i `handlers`-objektet i `tools/call`:
```javascript
my_new_tool: async () => {
  return { content: [{ type: "text", text: "Result" }] };
},
```

3. PA ville automatisk detektere den næste gang den kalder `tools/list`!

### MCP Protocol Flow

```
PA Client                          MCP Server (dad_jokes_mcp.js)
   │                                        │
   ├─ POST /mcp (initialize) ───────────────┤
   │                                        │
   │ ◄──── { serverInfo, capabilities } ────┤
   │                                        │
   ├─ POST /mcp (tools/list) ───────────────┤
   │                                        │
   │ ◄──── { tools: [...] } ────────────────┤
   │                                        │
   ├─ POST /mcp (tools/call) ───────────────┤
   │   { name: "get_multiple_jokes", ... }  │
   │                                        │
   │ ◄──── { result: { content: [...] } } ──┤
   │                                        │
```

## API Details

### MCP Endpoint: `/mcp`

**Method:** `POST`

**Headers:**
- `Content-Type: application/json`
- `Mcp-Session-Id` (response header - auto-generated)

**Request Format:** JSON-RPC 2.0

Example request body for calling a tool:
```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_random_joke",
    "arguments": {}
  }
}
```

# Dad Jokes MCP Server

A Model Context Protocol (MCP) server that fetches and manages dad jokes via Streamable HTTP transport. Jokes are automatically saved to persistent storage.

## Features

- 🎯 **Streamable HTTP Transport** - Uses MCP 2024-11-05 protocol
- 💾 **Persistent Storage** - Jokes automatically saved to `www/jokes.json`
- 😄 **Multiple Joke Sources** - Fetches from 9+ joke APIs
- 🐳 **Docker Ready** - Full Docker setup with volume mounts
- 🔧 **10 Tools**:
  - `get_random_joke` - Fetch a random dad joke and save it
  - `get_multiple_jokes` - Fetch multiple jokes at once
  - `server_status` - View server status, version, and stats
  - `get_all_jokes` - View all saved jokes
  - `clear_jokes` - Clear saved jokes
  - `get_joke_category` - Get jokes by category (Programming, Knock-knock, General, Chuck Norris)
  - `fill_jokes_batch` - Ensure at least N jokes are stored
  - `add_jokes` - Fetch and store N new jokes
  - `add_joke` - Add a custom joke manually
  - `clean_jokes` - Remove null/empty entries

## Quick Start

### Prerequisites
- Docker & Docker Compose
- Node.js 20+ (for local development)

### Run with Docker

```bash
docker compose up -d
```

The server will start on `http://localhost:5000/` (frontend) with MCP endpoint at `/mcp`

### MCP Configuration

Add to your MCP client config (Gemini, Page Assist, etc):

```json
{
  "mcp": {
    "servers": {
      "dad-jokes": {
        "url": "http://localhost:5000/mcp"
      }
    }
  }
}
```

## Testing

See [`docs/test.md`](docs/test.md) for the full test suite, JSON fixtures, and gotchas.

### Test with curl

```bash
# Initialize
curl -X POST http://localhost:5000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'

# List tools
curl -X POST http://localhost:5000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

# Get a random joke
curl -X POST http://localhost:5000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_random_joke","arguments":{}}}'
```

## Project Structure

```
dad-jokes-mcp/
├── dad_jokes_mcp.mjs        # Main server code
├── package.json             # Node.js dependencies
├── Dockerfile               # Docker image definition
├── docker-compose.yml       # Docker Compose config
├── .dockerignore            # Docker build exclusions
├── www/
│   └── jokes.json           # Persistent jokes storage
└── .git/                    # Git repository
```

## Jokes Storage

Jokes are automatically saved to `www/jokes.json` with the following structure:

```json
[
  "Chuck Norris can understand women.",
  "Why did the scarecrow win an award? He was outstanding in his field.",
  "..."
]
```

The file persists across container restarts due to Docker volume mounting.

## API Details

### MCP Endpoint: `/mcp`

**Method:** `POST`

**Headers:**

- `Content-Type: application/json`
- `Mcp-Session-Id` (response header - auto-generated)

**Request Format:** JSON-RPC 2.0

Example request body for calling a tool:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_random_joke",
    "arguments": {}
  }
}
```

## Environment Variables

- `PORT` - Server port (default: 5000)

## Docker Volumes

- `./www:/app/www` - Mounts local `www/` directory for persistent joke storage



## Development

### Local Development (without Docker)

```bash
npm install
PORT=5000 node dad_jokes_mcp.mjs
```

### Build Docker Image

```bash
docker compose build
```

### View Logs

```bash
docker logs dad-jokes-mcp-server -f
```

## Troubleshooting

### MCP Validation Failed

If you see "Legacy MCP SSE endpoints are not supported", ensure your MCP client is configured to use the Streamable HTTP endpoint:

```
http://localhost:5000/mcp  ✓ Correct
http://localhost:5000/sse  ✗ Deprecated (SSE)
```

### No Jokes Saved

- Check Docker volume mount: `docker compose config | grep volumes`
- Verify `www/` directory exists: `ls -la www/`
- Check container logs: `docker logs dad-jokes-mcp-server`

### API Connection Issues

- Ensure server is running: `curl http://localhost:5000/`
- Check port availability: `netstat -an | grep 5000`
- Verify firewall allows port 5000

## Performance

- Joke fetching: ~1-3 seconds (with retries)
- JSON parsing: <100ms
- File I/O: ~50-200ms depending on disk speed

## License

MIT

## Author

Michael G. Nielsen

---

**Dad jokes duh** 😄