A personal AI assistant Claude MCP setup gives you a private, scriptable counterpart that can read and write your files through the Model Context Protocol instead of brittle custom integrations. This tutorial builds a minimal but functional assistant: a Claude-backed agent that connects to a local MCP server exposing note-taking tools, then expands to a second utility tool.
Prerequisites
- Python 3.11 or newer
- Node.js 18+ (only if you later run JS MCP servers)
- An Anthropic API key in
ANTHROPIC_API_KEY - Familiarity with async Python
Install dependencies:
pip install anthropic mcp "mcp[cli]"
Verify the MCP CLI is present:
python -m mcp --version
Step 1: Scaffold the project
Create a directory and two files: server.py for the MCP server, assistant.py for the client.
mkdir claude-mcp-assistant && cd claude-mcp-assistant
touch server.py assistant.py
Step 2: Build an MCP server for notes
The Model Context Protocol standardizes tool exposure. We’ll use FastMCP, a high-level decorator-based server from the mcp package.
The server code
server.py:
from mcp.server.fastmcp import FastMCP
import os
mcp = FastMCP("notes")
NOTES_DIR = os.path.expanduser("~/mcp_notes")
@mcp.tool()
def write_note(title: str, content: str) -> str:
"""Write a note to the local notes directory."""
os.makedirs(NOTES_DIR, exist_ok=True)
path = os.path.join(NOTES_DIR, f"{title}.txt")
with open(path, "w") as f:
f.write(content)
return f"Saved to {path}"
@mcp.tool()
def read_note(title: str) -> str:
"""Read a previously written note by title."""
path = os.path.join(NOTES_DIR, f"{title}.txt")
if not os.path.exists(path):
return f"No note named {title}"
with open(path) as f:
return f.read()
if __name__ == "__main__":
mcp.run(transport="stdio")
Run and verify
Start the server in one terminal:
python server.py
It blocks on stdio. In another terminal, list tools using the MCP inspector (optional):
npx @modelcontextprotocol/inspector python server.py
You should see write_note and read_note registered. For this tutorial, we’ll verify through the client instead.
Step 3: Wire Claude to the MCP server
Claude’s tool-use API expects a list of tools with name, description, and input_schema. MCP returns the same shape under inputSchema. We translate field names and forward calls.
Client session and tool translation
assistant.py:
import asyncio
import os
from anthropic import Anthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
SERVER_PARAMS = StdioServerParameters(command="python", args=["server.py"])
def to_anthropic_tools(mcp_tools):
return [
{
"name": t.name,
"description": t.description,
"input_schema": t.inputSchema,
}
for t in mcp_tools
]
async def run_assistant(user_prompt: str):
async with stdio_client(SERVER_PARAMS) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
mcp_tools = (await session.list_tools()).tools
tools = to_anthropic_tools(mcp_tools)
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
messages = [{"role": "user", "content": user_prompt}]
while True:
resp = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=messages,
tools=tools,
)
if resp.stop_reason != "tool_use":
print(resp.content[0].text)
break
tool_block = next(b for b in resp.content if b.type == "tool_use")
result = await session.call_tool(tool_block.name, tool_block.input)
messages.append({"role": "assistant", "content": resp.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_block.id,
"content": result.content[0].text,
}
],
})
Run the loop
Add a runner at the bottom of assistant.py:
if __name__ == "__main__":
asyncio.run(run_assistant("Write a note titled 'todo' with content 'Buy milk and deploy patch'."))
Execute:
python assistant.py
Step 4: Expected output
First run, Claude should emit a tool call. The client executes write_note via MCP, returns the path, and Claude summarizes:
Saved to /Users/you/mcp_notes/todo.txt
I've saved a note titled "todo" with your reminder to buy milk and deploy the patch.
Check the file:
cat ~/mcp_notes/todo.txt
# Buy milk and deploy patch
That confirms the personal AI assistant Claude MCP loop works: natural language in, tool executed, result folded back.
Step 5: Add a second tool
Extend server.py with a time tool:
from datetime import datetime
@mcp.tool()
def current_time() -> str:
"""Return current local time as ISO string."""
return datetime.now().isoformat()
Restart the server. Update the prompt in assistant.py:
asyncio.run(run_assistant("What time is it? Write a note titled 'log' with that time."))
Claude will call current_time, then write_note. Output:
Saved to /Users/you/mcp_notes/log.txt
I recorded the current time (2025-03-14T11:02:33.123456) in your log note.
Production considerations
The stdio transport is fine for a single-user local assistant, but for remote or multi-process deployments use MCP over SSE or WebSocket. Keep tool schemas tight; Claude performs better when descriptions state side effects clearly.
If you front the model call with an OpenAI-compatible gateway such as n4n.ai, the MCP client code stays identical—you only change the Anthropic client’s base URL or swap to an OpenAI-style client. You then get automatic fallback when a provider is rate-limited or degraded, plus per-token usage metering, without touching the tool loop.
Cache control also matters: forward Anthropic’s cache_control hints on static tool schemas to avoid re-paying for large system prompts. MCP tool lists rarely change; mark them cacheable when your transport supports it.
Wrapping up
You now have a runnable personal AI assistant Claude MCP pattern: an MCP server exposing safe local tools, a thin translation layer to Claude’s tool API, and a replay loop that executes calls and feeds results back. From here, add authenticated APIs as MCP servers, or split the assistant into a daemon that watches a mailbox. The protocol is the backbone; Claude is the reasoning layer.