MCP Agent Harness Demo
A minimal demonstration of an LLM agent harness using the Model Context Protocol (MCP).
This repository contains small Node.js/TypeScript and Python examples showing how an agent can:
- discover tools from an MCP server;
- expose those tools to an LLM;
- let the model request tool calls;
- execute those calls through MCP;
- return tool results to the model;
- continue the loop until the model produces a final response.
Important: This is demonstration code only. It is not production code and should not be treated as a secure, hardened, or complete agent framework.
The purpose of the repository is to make the mechanics of an MCP-based agent harness easy to inspect.
Architecture
At a high level:
User
|
v
LLM
|
| tool request
v
Agent Harness
|
v
MCP Client
|
v
MCP Server
|
v
Tool Implementation
|
v
Tool Result
|
+------------------> LLM
The responsibilities are deliberately separated:
LLM - decides what it thinks should happen
Harness - manages the agent loop and conversation state
MCP - standardises tool discovery and invocation
Tools - perform the actual deterministic operations
MCP does not decide which tool should be called.
Tool selection remains a model decision unless the surrounding application explicitly constrains or overrides it.
Why This Repository Exists
A lot of agent-framework terminology can obscure what is actually happening.
The essential harness loop is little more than:
call model
|
v
did it request a tool?
|
/ \
no yes
| |
answer execute tool
|
v
return result
|
+----> call model again
This repository keeps that mechanism visible instead of hiding it behind a large agent framework.
Repository Layout
A typical layout is:
.
├── node/
│ ├── package.json
│ └── src/
│ ├── agent.ts
│ └── server.ts
│
└── python/
├── agent.py
└── server.py
The exact directory names can be changed without affecting the architecture.
Example MCP Tools
The demo server exposes three deliberately simple hypothetical tools:
get_github_activity
get_site_content
contact_scott
These are only examples intended to demonstrate:
- tool discovery;
- tool schemas;
- tool descriptions;
- arguments;
- execution;
- result handling.
They are not intended to represent a real backend.
Node.js / TypeScript
Requirements
- Node.js 20+
- an OpenAI API key
Install dependencies:
npm install
Set the API key:
export OPENAI_API_KEY="sk-..."
Run the agent:
npm start
The MCP server is launched automatically by the agent through the stdio transport.
You should not need to run the server separately.
Example output:
MCP tools: [
'get_github_activity',
'get_site_content',
'contact_scott'
]
MODEL REQUESTED TOOL: get_github_activity
ARGUMENTS: {}
MCP RESULT:
...
FINAL ANSWER
------------
Scott has recently been working on...
Python
Requirements
- Python 3.10+
- an OpenAI API key
Create a virtual environment:
python3 -m venv .venv
source .venv/bin/activate
Upgrade packaging tools:
python3 -m pip install --upgrade pip setuptools wheel
Install dependencies:
pip install "mcp>=2,<3" openai
Set the API key:
export OPENAI_API_KEY="sk-..."
Run:
python3 agent.py
The Python version runs as an interactive CLI chatbot:
MCP tools: ['get_github_activity', 'get_site_content', 'contact_scott']
Chat started.
Type /quit to exit.
You> hello
Assistant> Hello! How can I help?
You> What has Scott been working on?
[tool] get_github_activity({})
[result] ...
Assistant> Scott has recently been working on...
The Python client retains conversation history between turns and streams normal responses to the terminal.
Stdio Transport
These examples use MCP over stdio.
The agent launches the MCP server as a child process:
agent
|
+---- stdin/stdout ---- MCP server
This is convenient for local experimentation because there is:
- no separate server daemon;
- no HTTP endpoint;
- no port configuration;
- no additional authentication layer.
One important consequence is that an MCP stdio server must not write arbitrary debugging output to stdout.
stdout belongs to the MCP protocol.
Use stderr for diagnostics instead.
For example:
print("debug information", file=sys.stderr)
or in TypeScript:
console.error("debug information");
The Agent Harness
The essential harness logic is:
while True:
response = await model(...)
calls = find_tool_calls(response)
if not calls:
return
for call in calls:
result = await mcp.call_tool(
call.name,
call.arguments,
)
add_result_to_context(result)
A real harness may additionally implement:
permissions
timeouts
tool allowlists
human approval
rate limits
cost limits
logging
tracing
context pruning
retry policies
authentication
authorization
sandboxing
validation
auditing
error recovery
This demo intentionally does very little of that.
Tool Discovery
The harness does not need a hard-coded list of implementations.
Instead it asks the MCP server for its available tools.
Conceptually:
MCP server
|
| tools/list
v
Agent harness
The harness then exposes the resulting:
name
description
input schema
to the model.
If the MCP server later adds another tool, the harness can discover it without adding another custom dispatch branch.
That is one of the main architectural benefits MCP provides.
Tool Selection Is Not Guaranteed
This point is important.
Suppose the server provides:
contact_scott
with a description saying it should be used when somebody wants to hire or contact Scott.
A user may say:
Can I hire Scott for consulting?
The desired model behaviour is:
contact_scott(...)
But an LLM may instead produce an ordinary conversational response.
MCP does not solve that problem.
The decision:
Does this natural-language request imply this tool?
is still probabilistic model inference.
Tool descriptions improve routing behaviour, but they do not create formal guarantees.
If an action must happen deterministically, that requirement should be enforced in ordinary application logic rather than relying solely on an LLM instruction.
Why This Matters
Once the model requests a tool, the rest of the system can be deterministic:
model requests tool
|
v
validate arguments
|
v
check permission
|
v
execute function
|
v
return result
But the initial semantic decision may still be probabilistic.
This distinction is particularly important for consequential actions such as:
sending money
deleting data
changing permissions
submitting legal information
making purchases
sending messages
altering customer records
A production system should place explicit deterministic controls around actions with meaningful consequences.
Streaming
The Python CLI uses streaming so text appears as it is generated.
Without streaming:
You> explain virtual memory
<wait>
Assistant> Virtual memory is...
With streaming:
You> explain virtual memory
Assistant> Virtual memory is...
Streaming primarily improves perceived latency.
Tool-using turns may still take longer because they can require multiple model requests:
model request
|
v
tool call
|
v
MCP execution
|
v
tool result
|
v
second model request
Demo Code — Not Production Code
This repository is intentionally minimal.
It does not provide the safeguards expected of a production agent system.
Among other things, production code would need to consider:
- authentication;
- authorization;
- secret management;
- hostile tool inputs;
- prompt injection;
- output validation;
- tool-result validation;
- schema enforcement;
- resource limits;
- network isolation;
- subprocess security;
- user confirmation for consequential operations;
- audit logging;
- retry behaviour;
- failure recovery;
- cost controls;
- context growth;
- model-version changes;
- API-version changes;
- dependency pinning;
- observability;
- testing and evaluation;
- privacy and data-retention requirements.
Do not expose the example MCP server directly to untrusted users or use the example contact_scott pattern for real communications without adding appropriate validation, authentication, persistence, abuse protection, and error handling.
Again:
This repository is demo code intended for learning and experimentation, not production deployment.
MCP Is Not the Agent
It is useful to keep the layers separate:
MCP
!= LLM
MCP
!= agent
MCP
!= tool-selection logic
MCP
!= security policy
MCP is the protocol used to expose and invoke capabilities.
The harness manages the model/tool loop.
The model performs language inference.
The underlying tools perform the actual work.
A useful mental model is:
Agent System
=
Model
+
Harness
+
Tools
+
Context
+
Policy
MCP provides a standard interface between some of those components.
Why Not Just Call Functions Directly?
For three local functions in one application, you absolutely can.
For example:
TOOLS = {
"foo": foo,
"bar": bar,
}
may be simpler than MCP.
MCP becomes more interesting when capabilities need to be reusable across multiple clients:
MCP Server
/ | \
/ | \
/ | \
CLI agent IDE website
The tool provider becomes independent from any particular model host or application.
That is the main architectural reason to introduce MCP.
Suggested Experiments
Once the basic CLI works, useful experiments include:
run the same prompt repeatedly
change tool descriptions
change models
change system instructions
record selected tools
measure latency
measure token usage
add approval gates
add deliberately ambiguous prompts
add multiple MCP servers
introduce tool failures
introduce malformed results
limit maximum agent steps
One particularly useful test is to record:
prompt
selected tool
arguments
number of model calls
latency
final response
across repeated runs.
That makes it possible to examine how much variation comes from the model and how much behaviour can be controlled by the harness.
License
I just chose err whatever, do what you will. Nothing original here but if you bite your own ass - not my fault.
Final Note
The point of this code is not to provide another large agent framework.
It is to expose the machinery clearly enough that the core process can be understood:
Model proposes.
Harness controls.
MCP connects.
Tools execute.
Everything more sophisticated is built on top of that.