n4nAI

Building a personal AI assistant with Claude and MCP

Build a local personal AI assistant with Claude and MCP. Hands-on Python tutorial covering MCP server setup, tool schema translation, and a runnable client loop with expected output.

n4n Team2 min read489 words

Audio narration

Coming soon — every post will get a voice note here.

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.

Tagsclaudemcppersonal-assistanttutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All personal ai assistants posts →