Skip to main content
Glama
NisargKadam

transcripts

by NisargKadam
README.md
# Meeting Minutes Agent — LangGraph + local MCP servers + Pydantic

A small, terminal-only learning project. An AI agent that is an expert at
**minutes of meeting (MoM)** and meeting summarization. It reads meeting
transcripts through a **local MCP file server**, does arithmetic through a
**local MCP math server**, and **asks your permission** before running any tool.

What you will learn:

1. **How to write a local MCP server** with the official `mcp` SDK (`FastMCP`), run over stdio.
2. **The tool repository pattern** in LangGraph: one object that loads tools from every MCP server and hands them to the agent.
3. **Permissions on tool execution**: a LangGraph `interrupt()` pauses the graph, the terminal asks you `y / n / a`, and the graph resumes.
4. **Pydantic** for settings, tool catalog rows, permission policy, and MCP tool result schemas.

No UI. No database. Three graph nodes. That is deliberate.

## Architecture

```
                 python main.py (terminal)
                          |
                          v
   +---------------------------------------------------+
   |  LangGraph StateGraph                              |
   |                                                    |
   |   START -> agent --(tool calls?)--> permission_gate |
   |              ^                          |          |
   |              |                     interrupt()     |
   |              |                     y / n / a       |
   |              |                          v          |
   |              +------------------- run_tools        |
   |                                         |          |
   |   agent --(no tool calls)--> END        |          |
   +-----------------------------------------|----------+
                                             v
                                   ToolRepository  (agent/tool_repository.py)
                                   +-----------+-----------+
                                   | tool      | server    |
                                   | add       | math      |
                                   | read_...  | transcripts
                                   +-----------+-----------+
                                      |                  |
                            stdio     |                  |     stdio
                                      v                  v
                       mcp_servers/math_server.py   mcp_servers/file_server.py
                       add, subtract, multiply,     list_transcripts,
                       divide, average, percentage  read_transcript,
                                                    search_transcripts
                                                         |
                                                         v
                                                    transcripts/*.txt, *.md
```

## Project layout

```
main.py                     terminal chat loop (entry point)
agent/
  config.py                 Settings (pydantic-settings): model, paths, MCP server commands
  models.py                 ToolSpec, PermissionRequest, PermissionMode (pydantic)
  permissions.py            PermissionPolicy (allow / ask / deny) + terminal prompt
  tool_repository.py        ToolRepository: loads MCP tools, catalog, lookup by name
  graph.py                  build_graph(): agent -> permission_gate -> run_tools -> agent
  prompts.py                system prompt for the MoM expert
mcp_servers/
  file_server.py            FastMCP "transcripts" server (read-only file access)
  math_server.py            FastMCP "math" server
transcripts/                sample meeting transcripts (.txt and .md)
tests/                      pytest suite, no API key needed
```

## Setup

Python 3.11 or newer.

```bash
python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env               # then put your OpenAI key in .env
```

`.env`:

```dotenv
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4.1-mini
```

## Run

```bash
python main.py
```

You will see the tool catalog first, then a prompt. Try:

- `List the transcripts you can read.`
- `Write the minutes for the sprint planning meeting.`
- `Summarize the Q4 budget review and tell me the total savings agreed.`
- `Across all meetings, which action items are assigned to Daniel?`
- `What percentage of this week's support tickets were about checkout?`

When the agent wants to run a tool marked `ask`, the graph pauses:

```
------------------------------------------------------------
  Permission required: transcripts.read_transcript
  Arguments: {"filename": "2026-09-08-sprint-planning.txt"}
------------------------------------------------------------
  Run this tool? [y]es / [n]o / [a]lways for this session:
```

- `y` runs it once.
- `n` refuses; the LLM receives "Tool call declined by user." and adapts.
- `a` runs it and switches that tool to `allow` for the rest of the session.

## How it works

### 1. The MCP servers (`mcp_servers/`)

Each server is one file. `FastMCP` turns plain functions into MCP tools:
the type hints become the JSON schema and the docstring becomes the
description the LLM reads.

```python
mcp = FastMCP("math")

@mcp.tool(name="add")
def add_tool(a: float, b: float) -> float:
    """Add two numbers and return a + b."""
    return add(a, b)

if __name__ == "__main__":
    mcp.run(transport="stdio")
```

The file server returns pydantic models (`TranscriptInfo`, `TranscriptContent`,
`SearchHit`) and refuses anything outside `transcripts/` or not ending in
`.txt` / `.md`. The **server is the security boundary**: never trust tool
arguments, even from your own agent.

Inspect a server on its own with the MCP Inspector:

```bash
npx @modelcontextprotocol/inspector python mcp_servers/math_server.py
```

### 2. The tool repository (`agent/tool_repository.py`)

`ToolRepository.from_settings()` uses `MultiServerMCPClient` to start each
server as a subprocess, do the MCP handshake, and wrap every MCP tool as a
LangChain `BaseTool`. The repository remembers which server each tool came
from and what its permission is:

```
Tool repository
  TOOL                 SERVER       PERMISSION  DESCRIPTION
  list_transcripts     transcripts  allow       List the meeting transcript files ...
  read_transcript      transcripts  ask         Read the full text of one transcript ...
  add                  math         ask         Add two numbers and return a + b.
```

The graph only ever calls `repo.tools`, `repo.get(name)` and `repo.server_of(name)`.
Add a third server in `Settings.mcp_connections()` and the graph does not change.

### 3. The permission gate (`agent/graph.py`)

```python
answer = interrupt(request.model_dump())   # graph pauses here
```

`interrupt()` saves the graph state in the checkpointer and returns control
to `main.py`, which shows the request and calls
`graph.ainvoke(Command(resume="yes"), config)`. The node re-runs from the
top and this time `interrupt()` returns `"yes"`. Declined calls get a
`ToolMessage` so the conversation history stays valid for the LLM, and
`run_tools` only executes calls that have no `ToolMessage` yet.

### 4. Pydantic (`agent/models.py`, `agent/permissions.py`, `agent/config.py`)

- `PermissionMode = Literal["allow", "ask", "deny"]` — a typo like `"maybe"` fails at construction.
- `PermissionPolicy` — `rules` per tool plus a `default`. Edit `DEFAULT_POLICY` to change behaviour.
- `PermissionRequest.model_dump()` — the JSON-safe payload passed through `interrupt()`.
- `Settings(BaseSettings)` — reads `.env` for you.

## Tests

```bash
pytest -q
```

| File | What it proves |
|------|----------------|
| `tests/test_math_server.py` | arithmetic and error cases, no MCP involved |
| `tests/test_file_server.py` | listing, reading, extension and path-traversal rejection, search |
| `tests/test_mcp_integration.py` | both servers really start over stdio and answer tool calls |
| `tests/test_permissions.py` | policy defaults, rules, session upgrade, terminal answer parsing |
| `tests/test_tool_repository.py` | catalog rows and lookup |
| `tests/test_graph.py` | interrupt payload, yes / no / always / allow / deny behaviour with a scripted fake LLM |

None of the tests call OpenAI.

## Exercises for students

1. **Change the policy.** In `agent/permissions.py` set `read_transcript` to `allow` and `divide` to `deny`. Run the agent and ask it to divide something.
2. **Add a tool.** Add `word_count(filename)` to `file_server.py`. Restart the agent and watch it appear in the catalog with no agent code changes.
3. **Add a write tool.** Add `save_minutes(filename, markdown)` that writes into an `output/` folder. Keep it `ask`. Why should write tools never be `allow`?
4. **Add a third server.** Create `mcp_servers/datetime_server.py` with a `days_between(a, b)` tool and register it in `Settings.mcp_connections()`.
5. **Swap the transport.** Run a server with `mcp.run(transport="streamable-http")` and change its connection to `{"transport": "streamable_http", "url": ...}`.