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": ...}`.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues