Skip to content

AlleninTaipei/MCPChat

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MCP Chat

This project is based on and modified from the Anthropic online learning course. The original used Anthropic's Claude via Google Vertex AI; this version has been migrated to use the OpenAI API.

MCP Chat is a command-line interface application for interactive AI chat, powered by OpenAI models. It supports document retrieval via @mentions, command-based prompts via /commands, and extensible tool integrations through the MCP (Model Context Protocol) architecture.

Prerequisites

Setup

Step 1: Configure environment variables

Create or edit the .env file in the project root:

OPENAI_API_KEY="sk-..."       # Your OpenAI API key
OPENAI_MODEL="gpt-4o-mini"    # Or any other OpenAI model (e.g. gpt-4o)
USE_UV=1                       # Set to 0 if not using uv

Step 2: Install dependencies

Option 1: Using uv (Recommended)

uv is a fast Python package installer and resolver.

  1. Install uv if not already installed:
pip install uv
  1. Install dependencies:
uv sync
  1. Run the project:
uv run main.py

Option 2: Using plain pip

  1. Create and activate a virtual environment:
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
  1. Install dependencies:
pip install openai python-dotenv prompt-toolkit "mcp[cli]>=1.8.0"
  1. Run the project:
python main.py

Usage

Basic Chat

Type your message and press Enter:

> What is the capital of France?

Document Retrieval

Use @ followed by a document ID to include a document's content in your query:

> Summarize @deposition.md

Documents are injected directly into the prompt — no extra tool call needed.

Commands

Use / to run predefined prompt commands defined in the MCP server:

> /summarize deposition.md

Press Tab to autocomplete available commands and document IDs.

Loading Additional MCP Servers

Pass extra server scripts as arguments when starting the app:

uv run main.py my_custom_server.py

MCP Inspector

MCP Inspector is a browser-based tool for inspecting and testing your MCP server — you can browse available tools, resources, and prompts, and call them interactively without running the full app.

To launch it:

uv run mcp dev mcp_server.py

Then open the URL shown in the terminal (usually http://localhost:5173) in your browser.

Tips for using the Inspector

  • When calling read_doc_contents, use the full filename including extension as the doc_id, e.g. deposition.md.
  • Available document IDs are: deposition.md, report.pdf, financials.docx, outlook.pdf, plan.md, spec.txt.

Why edits don't appear in mcp_server.py

You might notice that after calling edit_document in the Inspector, the docs dictionary inside mcp_server.py doesn't change. This is expected — and understanding why is a useful lesson.

The docs dict lives in memory (RAM), not in the file. When the server starts, Python reads mcp_server.py and loads docs into memory. edit_document modifies that in-memory copy, which is why a subsequent read_doc_contents call returns the updated content. But the source file itself is never touched — it's just the recipe book. The cook writes on a notepad, not back into the book.

When you restart the server, the notepad is thrown away and docs is reloaded from the original source file — so all edits disappear.

This is a known limitation of the current design. If you want edits to persist across restarts, docs would need to be backed by a real store (a JSON file, SQLite, etc.) rather than an in-memory dict.

End-to-end testing

The fastest way to verify the whole system is working is to run through these five cases in order:

1. Basic chat — confirms OpenAI connectivity

> What is 1 + 1?

2. @ document injection — confirms MCP server is running and document content is injected into the prompt

> What is @deposition.md about?

3. Tool use — confirms the agentic loop, ToolManager, and MCP tool execution are all working

> Use the read_doc_contents tool to read report.pdf and tell me what it says.

4. Tool use + edit — confirms multi-turn tool use and in-memory edits

> Use edit_document to change the word "condenser" to "cooling" in report.pdf, then read it back to confirm.

5. / command — confirms MCP Prompt retrieval and the full command pipeline

> /format financials.docx

Each case exercises a different layer of the stack. If all five pass, the entire end-to-end flow is verified.

What happens during /format

The /format command is a good illustration of how all the layers work together:

  1. _process_command() in core/cli_chat.py detects the / prefix and fetches the format prompt from the MCP server
  2. The prompt instructs the model to reformat the document using Markdown and use the edit_document tool to save the result
  3. The model calls edit_document — the agentic loop in core/chat.py detects finish_reason == "tool_calls" and executes the tool
  4. The result is added back to the message history, the loop runs again, and the model outputs the final formatted document

One thing to be aware of: if the original document is very short (like financials.docx, which is just one sentence), the model may invent plausible-sounding content — budget figures, categories, etc. — that doesn't exist in the source. This is normal LLM behaviour: when information is sparse, the model fills in what seems reasonable rather than saying "I don't know."

In real applications, guard against this by adding an explicit instruction to the prompt: only use information already present in the document, do not add anything new.

Why you only need to run main.py

You might wonder why mcp_server.py and mcp_client.py don't need to be started separately. The answer is in main.py — it launches mcp_server.py automatically as a subprocess:

command, args = ("uv", ["run", "mcp_server.py"])

doc_client = await stack.enter_async_context(
    MCPClient(command=command, args=args)
)

MCPClient.connect() hands this command to the OS, which starts mcp_server.py as a background process. The two processes then talk to each other over stdin / stdout — this is MCP's stdio transport.

You run: uv run main.py
              │
              ├── spawns subprocess ──→ mcp_server.py (background process)
              │                               ↑ ↓  stdin / stdout
              └── MCPClient ─────────────────┘
                  (mcp_client.py, imported as a class)
  • mcp_server.py is launched by main.py — you never start it manually
  • mcp_client.py is imported as a class — it's not a script you run
  • When main.py exits, the subprocess is cleaned up automatically via AsyncExitStack

The reason stdin / stdout is used as the transport is that it makes MCP servers language-agnostic — the server could be written in Python, Node.js, or Rust. As long as it speaks the MCP protocol over standard I/O, the client doesn't care.


Development

Adding New Documents

Edit the docs dictionary in mcp_server.py to add new documents.

Adding New MCP Servers

Any Python script that implements the MCP protocol can be passed as an argument to main.py. Each server runs as its own subprocess and exposes its tools automatically to the model.

Linting and Type Checks

No lint or type checks are currently configured.


Bonus: What Are uv and venv?

Let's Start With an Analogy

Imagine you're a chef with many recipes (projects), each calling for different ingredients (packages).

Here's the problem: recipe A calls for "soy sauce v1.0," recipe B calls for "soy sauce v2.0," and the two versions are incompatible, their APIs don't match. If your kitchen (your computer) only has one bottle of soy sauce, you're stuck.

venv and uv both exist to solve this problem.

What Is venv?

venv is a tool built into Python for creating a "virtual environment".

The idea behind a virtual environment is simple: it creates an independent copy of the Python runtime inside your project folder, so the packages installed for this project are completely isolated from other projects and don't interfere with each other.

python -m venv .venv        # Create a virtual environment (creates the .venv folder)
source .venv/bin/activate   # "Enter" this environment
pip install openai          # Now openai is installed only for this project

The openai package you install in this project won't affect any other project on your computer. That's what "isolation" means.

The .venv folder holds that independent copy of Python plus all its packages, which is why it's often several hundred MB in size: it really is a complete runtime environment.

So What Is uv?

uv is a more modern tool that replaces the combination of pip and venv.

Think of it as venv + pip + package version locking, all bundled into one faster, smarter tool.

# The old way (three steps)
python -m venv .venv
source .venv/bin/activate
pip install openai

# The uv way (one step)
uv add openai

What Makes uv Better?

1. Ridiculously Fast

pip installs packages one at a time, downloading and installing sequentially. uv is written in Rust and works almost entirely in parallel, making it 10 to 100 times faster.

2. Automatic .venv Management

You don't need to manually run python -m venv .venv and then activate it. Running uv run main.py automatically finds (or creates) the right virtual environment and runs your script inside it.

3. Lockfile (Version Locking)

The uv.lock file in the project records the exact version of every package. This means that even if you switch computers months later, or hand the project to a colleague, running uv sync restores the exact same environment, avoiding the classic "works on my machine" problem.

What Actually Happens Behind uv run python -c?

uv is a Python package and project manager developed by the Astral team, and it has become extremely popular in the Python ecosystem recently. Astral is also the team behind the well-known Python linter Ruff. uv itself is written in Rust, so its core characteristics are speed, simplicity, and reproducibility.

Core Tool: uv

uv's goal is to replace common tools like pip, pip-tools, venv, and pyenv. Its most notable feature is extreme speed, often 10 to 100 times faster than traditional Python package management tools.

  • Powered by Rust: it takes advantage of Rust's concurrency and efficient memory management, making package resolution and installation very fast.
  • A single binary: uv is a standalone tool, so you don't need a pre-configured Python environment before using it.
  • Global cache: uv uses content-addressed storage. If multiple projects use the same package, only one copy needs to be kept on disk; when building an environment, uv links it into the project via hard links, which keeps installation fast and saves disk space.

Breaking Down the Command: uv run

uv run is one of uv's most powerful features, which you can think of as "on-demand environment management".

  • Automatic virtual environment creation: when you run uv run, uv checks whether a usable virtual environment already exists in the current directory. If not, it automatically creates .venv and ensures the dependencies match the project configuration.
  • Seamless script dependency integration: if a Python script includes metadata following PEP 723 at the top, uv run can automatically prepare the packages the script needs based on that metadata. Once the script finishes, there's no need to manually clean up any temporary environments.

Breaking Down the Command: python -c

python -c is a standard Python feature meaning "execute the following string as Python code".

uv run python -c "print('hello from uv')"

Here, -c stands for command, and it's commonly used for quick tests, one-liner scripts, or embedding a small piece of Python logic in a shell script.

Putting It Together: uv run python -c "..."

Here's what happens when you run this command:

uv run python -c "..."

It triggers a modern Python execution workflow:

  1. Environment preparation: uv quickly confirms that the current project has the correct Python virtual environment.
  2. Dependency resolution: if the code or script metadata requires specific packages, uv ensures they are installed.
  3. Code execution: uv runs the code after python -c inside the isolated project environment.
  4. Consistency: as long as uv is available on the machine, the same command is more likely to produce consistent results across different computers, without needing to manually run pip install or activate a virtual environment first.

Visual Summary

Your computer
│
├── Project A/
│   └── .venv/          ← Project A's independent environment
│       └── openai 1.0
│
├── Project B/
│   └── .venv/          ← Project B's independent environment
│       └── openai 2.0
│
└── System Python        ← Best to avoid installing anything here

Each .venv is its own independent bubble, isolated from the others. uv is the modern tool that manages these bubbles for you.

The Commands You Actually Need to Remember

Scenario Command
Set up the project for the first time uv sync
Add a package uv add openai
Remove a package uv remove anthropic
Run the program uv run main.py

No manual activation. No fiddling with pip. uv handles everything.

About

An AI-powered MCP chat client demonstrating how to connect LLMs with tools and external systems using the Model Context Protocol.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages