Build a Personal Notes MCP Server
Build a Python MCP server that lets Claude manage your notes via natural language.
Introduction
30 Second Summary
AI assistants are powerful on their own. They become truly useful when they can reach into your personal tools and data on demand.
In this project, you will build an MCP server in Python that gives Claude Desktop the ability to create, search, and manage your notes through natural language.
What You'll Build
A Python MCP server that exposes tools, resources, and prompts for managing local notes, connected to Claude Desktop for AI-powered interaction.
By the end of this project, you'll have:
- A working MCP server that Claude Desktop can discover and interact with through natural language.
- Firsthand visibility into the tool-calling protocol by inspecting raw JSON-RPC messages in the MCP Inspector.
- A project demonstrating all three MCP primitives (tools, resources, and prompts) with a practical use case.
- Secret Mission: Add note statistics and tag filtering to make your server production-ready.
Are there any prerequisites?
You should be familiar with Python basics (functions, dictionaries, file I/O). A general understanding of what LLMs are is helpful, but you do not need prior experience with MCP or tool calling.
Build the MCP Server with Tools
Your environment is ready. Now it is time to build the core of your project: the MCP server file that Claude will eventually connect to.
In MCP, tools are functions that the AI model can actively decide to call. You will create three tools that let Claude create, search, and delete notes on your machine. Each tool is a plain Python function with a decorator that makes it discoverable through the protocol.
In this step, get ready to:
- Create the server file with imports, configuration, and helper functions.
- Implement three tools that Claude can call to manage notes.
- Add the server entry point and confirm it starts without errors.
Create the server file
The server file needs a FastMCP instance (which handles protocol details for you), a file path for storing notes, and two helper functions for reading and writing JSON data.
- In VS Code, open the notes-mcp-server folder by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the notes-mcp-server folder, and click Open.
- Create a new file by clicking File then New File.
- Save it immediately as notes_server.py by pressing Ctrl+S (Windows) or Cmd+S (macOS) and typing the filename.
- Paste the following code into notes_server.py:
import json
import os
from datetime import datetime
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("notes")
NOTES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "notes.json")
def load_notes():
"""Load notes from the JSON file."""
if os.path.exists(NOTES_FILE):
with open(NOTES_FILE, "r") as f:
return json.load(f)
return []
def save_notes(notes):
"""Save notes to the JSON file."""
with open(NOTES_FILE, "w") as f:
json.dump(notes, f, indent=2)
What does this code do?
The first four lines import the libraries you need. FastMCP comes from the MCP SDK you installed in Step 1.
mcp = FastMCP("notes") creates your server instance. The string "notes" is the server name that Claude will see when it discovers your server.
NOTES_FILE builds a path to notes.json in the same folder as your script. The two helper functions handle loading and saving notes as a Python list of dictionaries.
- Save notes_server.py.
- You will not see any output yet. The tools you add next will use these helpers.
Seeing a red underline on the import?
If VS Code shows an error under from mcp.server.fastmcp import FastMCP, the MCP SDK may not be installed in the Python environment VS Code is using.
- Open the VS Code terminal with Ctrl+` (Windows) or Cmd+` (macOS).
- Run pip install "mcp[cli]" again inside this terminal to install it for the active interpreter.
help me fix my MCP SDK import error
Implement the three tools
Each tool is a Python function decorated with @mcp.tool(). The function's docstring becomes the description that Claude reads to decide when to use the tool. Clear docstrings are critical for good tool selection.
- Add the create_note tool below the save_notes function in notes_server.py:
@mcp.tool()
def create_note(title: str, content: str, tags: list[str] | None = None) -> str:
"""Create a new note with a title, content, and optional tags.
Args:
title: The title of the note
content: The body content of the note
tags: Optional list of tags to categorize the note
"""
notes = load_notes()
note = {
"id": len(notes) + 1,
"title": title,
"content": content,
"tags": tags or [],
"created_at": datetime.now().isoformat(),
}
notes.append(note)
save_notes(notes)
return f"Note '{title}' created successfully with ID {note['id']}."
What does this tool do?
When Claude decides to create a note, it calls this function with a title, content, and optional tags. The function loads existing notes from notes.json, assigns a new ID, appends the note, and saves everything back.
The Args: section in the docstring tells Claude exactly what each parameter is for. This is how the model knows what arguments to pass.
- You will not see a change yet. The next tool adds search capability.
- Add the search_notes tool below create_note:
@mcp.tool()
def search_notes(query: str) -> str:
"""Search notes by title or content.
Args:
query: The search term to look for in note titles and content
"""
notes = load_notes()
query_lower = query.lower()
matches = [
n
for n in notes
if query_lower in n["title"].lower() or query_lower in n["content"].lower()
]
if not matches:
return "No notes found matching your query."
results = []
for note in matches:
tags_str = ", ".join(note["tags"]) if note["tags"] else "none"
results.append(
f"[ID: {note['id']}] {note['title']}\n"
f" Tags: {tags_str}\n"
f" {note['content'][:100]}"
)
return "\n---\n".join(results)
What does this tool do?
This tool performs a case-insensitive search across note titles and content. It returns formatted results showing the ID, title, tags, and a preview of each matching note.
Claude uses this when a user asks something like "find my notes about Python." The model reads the docstring, decides search_notes is the right tool, and passes the search term as the query argument.
- You will not see a change yet. One more tool to add.
- Add the delete_note tool below search_notes:
@mcp.tool()
def delete_note(note_id: int) -> str:
"""Delete a note by its ID.
Args:
note_id: The unique ID of the note to delete
"""
notes = load_notes()
original_count = len(notes)
notes = [n for n in notes if n["id"] != note_id]
if len(notes) == original_count:
return f"No note found with ID {note_id}."
save_notes(notes)
return f"Note {note_id} deleted successfully."
What does this tool do?
This tool filters out the note matching the given ID and saves the remaining list. If the list length did not change, it means no note was found with that ID, so it returns an error message instead.
Together, these three tools give Claude full CRUD capability over your notes (minus update, which you could add as a bonus later).
- Save notes_server.py.
- You will not see output yet. The entry point you add next lets you run the server.
Add the server entry point and test
The entry point tells Python to start the MCP server using stdio transport when you run the file directly. Stdio means the server communicates via standard input and output using JSON-RPC messages.
- Add this block at the very bottom of notes_server.py, below the delete_note function:
if __name__ == "__main__":
mcp.run(transport="stdio")
What does this code do?
The if __name__ == "__main__": guard ensures the server only starts when you run the file directly (not when it is imported). mcp.run(transport="stdio") launches the server, which then listens for JSON-RPC messages on stdin and responds on stdout.
- Save notes_server.py.
- Open the VS Code terminal with Ctrl+` (Windows) or Cmd+` (macOS).
- Test that the server starts without errors by running:
python notes_server.py
- The terminal will appear to hang with a blinking cursor. This is the expected behaviour. The server is running and waiting for JSON-RPC input.
- Press Ctrl+C to stop the server.
If the server started and waited without printing any errors, your code is correct.
Seeing an error when running the server?
- ModuleNotFoundError: The MCP SDK is not installed for the Python version your terminal is using. Run pip install "mcp[cli]" and try again.
- SyntaxError: Check that your Python version is 3.10 or higher (python --version). The list[str] | None syntax requires Python 3.10+.
- IndentationError: Make sure you did not mix tabs and spaces. VS Code shows this as wavy underlines in the editor.
help me fix my MCP server startup error
✔️ Awesome, I've got everything!
Great. Double check you have saved notes_server.py and that the server started without errors.
ⓧ I'd like to double check the full code
Here is the complete notes_server.py at this point. Compare your file against it:
import json
import os
from datetime import datetime
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("notes")
NOTES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "notes.json")
def load_notes():
"""Load notes from the JSON file."""
if os.path.exists(NOTES_FILE):
with open(NOTES_FILE, "r") as f:
return json.load(f)
return []
def save_notes(notes):
"""Save notes to the JSON file."""
with open(NOTES_FILE, "w") as f:
json.dump(notes, f, indent=2)
@mcp.tool()
def create_note(title: str, content: str, tags: list[str] | None = None) -> str:
"""Create a new note with a title, content, and optional tags.
Args:
title: The title of the note
content: The body content of the note
tags: Optional list of tags to categorize the note
"""
notes = load_notes()
note = {
"id": len(notes) + 1,
"title": title,
"content": content,
"tags": tags or [],
"created_at": datetime.now().isoformat(),
}
notes.append(note)
save_notes(notes)
return f"Note '{title}' created successfully with ID {note['id']}."
@mcp.tool()
def search_notes(query: str) -> str:
"""Search notes by title or content.
Args:
query: The search term to look for in note titles and content
"""
notes = load_notes()
query_lower = query.lower()
matches = [
n
for n in notes
if query_lower in n["title"].lower() or query_lower in n["content"].lower()
]
if not matches:
return "No notes found matching your query."
results = []
for note in matches:
tags_str = ", ".join(note["tags"]) if note["tags"] else "none"
results.append(
f"[ID: {note['id']}] {note['title']}\n"
f" Tags: {tags_str}\n"
f" {note['content'][:100]}"
)
return "\n---\n".join(results)
@mcp.tool()
def delete_note(note_id: int) -> str:
"""Delete a note by its ID.
Args:
note_id: The unique ID of the note to delete
"""
notes = load_notes()
original_count = len(notes)
notes = [n for n in notes if n["id"] != note_id]
if len(notes) == original_count:
return f"No note found with ID {note_id}."
save_notes(notes)
return f"Note {note_id} deleted successfully."
if __name__ == "__main__":
mcp.run(transport="stdio")
Your MCP server can now create, search, and delete notes. Next up, you will add resources and a prompt to complete all three MCP primitives.
Add Resources and a Prompt
Your server already has tools that Claude can call to create, search, and delete notes. Tools are actions, but sometimes Claude needs passive data to work with.
In this step, you will add the other two MCP primitives: resources (read-only data endpoints) and a prompt (a reusable template that guides Claude on how to use your server). Together, all three primitives give Claude a complete picture of what your server offers.
In this step, get ready to:
- Add two resources that expose your notes data as read-only views.
- Add a prompt template that guides Claude on how to summarize your notes.
- Verify all three MCP primitives work together without errors.
Add a resource to view all notes
Resources are read-only data that the host application (like Claude Desktop) can attach as context to a conversation. They are similar to GET endpoints in a REST API.
- In notes_server.py, find the if __name__ == "__main__": block at the bottom of your file.
- Add the following code above that if __name__ block:
@mcp.resource("notes://all")
def get_all_notes() -> str:
"""Get all notes as formatted text."""
notes = load_notes()
if not notes:
return "No notes stored yet."
results = []
for note in notes:
tags_str = ", ".join(note["tags"]) if note["tags"] else "none"
results.append(
f"[ID: {note['id']}] {note['title']}\n"
f" Tags: {tags_str}\n"
f" Created: {note['created_at']}\n"
f" {note['content']}"
)
return "\n---\n".join(results)
What does this code do?
The @mcp.resource("notes://all") decorator registers get_all_notes as a resource at the URI notes://all.
When a client reads this resource, the function loads all notes from disk and formats them as human-readable text. Unlike tools, resources don't take action. They just serve data.
You won't see a change yet. The next chunk adds the second resource, then you will verify everything together.
Add a resource to view all tags
- Directly below the get_all_notes function you just added, paste this second resource:
@mcp.resource("notes://tags")
def get_all_tags() -> str:
"""Get all unique tags used across notes."""
notes = load_notes()
all_tags = set()
for note in notes:
all_tags.update(note.get("tags", []))
if not all_tags:
return "No tags found."
return ", ".join(sorted(all_tags))
What does this code do?
This resource at notes://tags collects every unique tag from all your notes and returns them as a sorted, comma-separated list.
Claude can read this resource to understand what categories exist in your notes before deciding which tool to call.
You won't see a change yet. The next chunk adds the prompt template, and then you will verify the server still starts.
How are resources different from tools?
Tools are functions that the model actively decides to call. They perform actions like creating or deleting notes.
Resources are passive data that the host application can attach as context. Think of them like read-only files the model can look at, but it cannot change them.
Add a prompt template
Prompts are user-triggered templates (like slash commands) that guide the model on how to use your server's capabilities.
- Below the get_all_tags function, add this prompt (still above the if __name__ block):
@mcp.prompt()
def summarize_notes() -> str:
"""Generate a prompt asking Claude to summarize and analyze all notes."""
return (
"Please read all my notes using the notes://all resource and provide:\n"
"1. A brief summary of each note\n"
"2. Common themes or topics across notes\n"
"3. Any suggested actions or follow-ups based on the content"
)
What does this code do?
The @mcp.prompt() decorator registers summarize_notes as a prompt template. When a user selects this prompt in Claude Desktop, it pre-fills the chat with instructions telling Claude to read all notes and provide a structured summary.
The docstring becomes the description that helps Claude Desktop display the prompt to the user.
- Save notes_server.py.
- Verify the server still starts by running:
python notes_server.py
- The server should start and wait for input with no error messages. Press Ctrl+C to stop it.
Seeing an error when you run the server?
- Check that each decorator has parentheses: @mcp.resource("notes://all") not @mcp.resource without the URI argument.
- Make sure all three new functions are placed above the if __name__ == "__main__": block. Python reads top to bottom, so anything after mcp.run() never registers.
- Check for mismatched quotes in the f-strings inside get_all_notes. Each line continuation needs matching parentheses.
Ask for help: Why is my MCP server throwing an error after I added resources and a prompt?
✔️ Awesome, I've got everything!
Great. Double check you have saved notes_server.py and confirmed it starts without errors.
ⓧ I'd like to double check the full code
Here is the complete notes_server.py including all tools, resources, and the prompt:
import json
import os
from datetime import datetime
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("notes")
NOTES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "notes.json")
def load_notes():
"""Load notes from the JSON file."""
if os.path.exists(NOTES_FILE):
with open(NOTES_FILE, "r") as f:
return json.load(f)
return []
def save_notes(notes):
"""Save notes to the JSON file."""
with open(NOTES_FILE, "w") as f:
json.dump(notes, f, indent=2)
@mcp.tool()
def create_note(title: str, content: str, tags: list[str] | None = None) -> str:
"""Create a new note with a title, content, and optional tags.
Args:
title: The title of the note
content: The body content of the note
tags: Optional list of tags to categorize the note
"""
notes = load_notes()
note = {
"id": len(notes) + 1,
"title": title,
"content": content,
"tags": tags or [],
"created_at": datetime.now().isoformat(),
}
notes.append(note)
save_notes(notes)
return f"Note '{title}' created successfully with ID {note['id']}."
@mcp.tool()
def search_notes(query: str) -> str:
"""Search notes by title or content.
Args:
query: The search term to look for in note titles and content
"""
notes = load_notes()
query_lower = query.lower()
matches = [
n
for n in notes
if query_lower in n["title"].lower() or query_lower in n["content"].lower()
]
if not matches:
return "No notes found matching your query."
results = []
for note in matches:
tags_str = ", ".join(note["tags"]) if note["tags"] else "none"
results.append(
f"[ID: {note['id']}] {note['title']}\n"
f" Tags: {tags_str}\n"
f" {note['content'][:100]}"
)
return "\n---\n".join(results)
@mcp.tool()
def delete_note(note_id: int) -> str:
"""Delete a note by its ID.
Args:
note_id: The unique ID of the note to delete
"""
notes = load_notes()
original_count = len(notes)
notes = [n for n in notes if n["id"] != note_id]
if len(notes) == original_count:
return f"No note found with ID {note_id}."
save_notes(notes)
return f"Note {note_id} deleted successfully."
@mcp.resource("notes://all")
def get_all_notes() -> str:
"""Get all notes as formatted text."""
notes = load_notes()
if not notes:
return "No notes stored yet."
results = []
for note in notes:
tags_str = ", ".join(note["tags"]) if note["tags"] else "none"
results.append(
f"[ID: {note['id']}] {note['title']}\n"
f" Tags: {tags_str}\n"
f" Created: {note['created_at']}\n"
f" {note['content']}"
)
return "\n---\n".join(results)
@mcp.resource("notes://tags")
def get_all_tags() -> str:
"""Get all unique tags used across notes."""
notes = load_notes()
all_tags = set()
for note in notes:
all_tags.update(note.get("tags", []))
if not all_tags:
return "No tags found."
return ", ".join(sorted(all_tags))
@mcp.prompt()
def summarize_notes() -> str:
"""Generate a prompt asking Claude to summarize and analyze all notes."""
return (
"Please read all my notes using the notes://all resource and provide:\n"
"1. A brief summary of each note\n"
"2. Common themes or topics across notes\n"
"3. Any suggested actions or follow-ups based on the content"
)
if __name__ == "__main__":
mcp.run(transport="stdio")
Your server now implements all three MCP primitives: tools for actions, resources for data, and a prompt for guided interaction. Next up, you will use the MCP Inspector to see the raw protocol messages flowing between a client and your server.
Test with the MCP Inspector
Your MCP server is fully built with tools, resources, and a prompt. But how do you know they actually work before connecting to Claude?
The MCP Inspector is a browser-based debugging tool that connects to your server and lets you interact with it directly. It shows the raw JSON-RPC messages flowing between client and server, giving you visibility into exactly how tool discovery and execution work under the hood.
In this step, get ready to:
- Launch the MCP Inspector and connect it to your server.
- Test all three tools and both resources through the Inspector UI.
- Observe the JSON-RPC protocol messages that power MCP communication.
Launch the Inspector
The MCP Inspector runs directly through npx and connects to your server via the stdio transport. It launches a web UI where you can interact with every capability your server exposes.
- Switch back to your terminal in VS Code.
- Make sure you are inside the notes-mcp-server folder.
- Launch the Inspector by running this command:
npx @modelcontextprotocol/inspector python notes_server.py
What does this command do?
This tells npx to download and run the MCP Inspector package. The Inspector then spawns your Python server as a child process, communicating with it over stdin/stdout (the stdio transport).
The Inspector acts as an MCP client, just like Claude Desktop would. It sends the same protocol messages your server will receive in production.
- Wait for the terminal to show that the Inspector is running. You should see output indicating it started on port 6274.
- Open your browser and navigate to http://localhost:6274.
You should see the MCP Inspector interface load with a server connection pane on the left side.
Inspector not loading?
- Make sure you stopped any previous python notes_server.py process before running the Inspector command. The Inspector will start the server itself.
- If you see a port conflict error, another process may be using port 6274. Close other development servers and try again.
- If npx prompts you to install the package, type y and press Enter to confirm.
Still stuck? Help me debug why the MCP Inspector won't start
Test your tools and resources
The Inspector UI has tabs for each MCP primitive. You will use these to call your tools, read your resources, and confirm everything works before connecting to Claude Desktop.
- Click the Tools tab in the Inspector.
- You should see your three tools listed: create_note, search_notes, and delete_note. Each one shows its description and input schema.
- Select create_note from the tools list.
- Fill in the form fields with test data:
- Set title to Learning MCP.
- Set content to Today I learned how MCP tools work.
- Set tags to ["learning", "mcp"].
- Click the button to execute the tool call.
- You should see a success response showing the note was created with an ID.
What should I see?
The tool result should display something like: Note 'Learning MCP' created successfully with ID 1. This confirms your tool executed and wrote to the notes.json file.
Now test the resources to confirm they return the data you just created.
- Click the Resources tab.
- You should see notes://all and notes://tags listed.
- Select notes://all and read its contents. You should see the note you just created, including its title, content, tags, and creation timestamp.
- Select notes://tags and read it. You should see learning, mcp listed as available tags.
- Click the Prompts tab.
- You should see summarize_notes listed with its description.
Tools or resources not showing up?
- Check that the Inspector successfully connected to your server. The connection pane should show an active connection.
- If the tools list is empty, there may be a syntax error in notes_server.py that prevents it from starting. Check the terminal for error messages.
- Try clicking a refresh or reconnect button in the Inspector to re-establish the connection.
Need help? My tools aren't appearing in the MCP Inspector
Observe the protocol
The Inspector does not just let you call tools. It also shows the raw protocol messages flowing between the client (Inspector) and your server. This is the same communication that happens when Claude Desktop uses your server.
- Look at the notifications pane in the Inspector (the panel showing server logs and notifications).
- Notice the tools/list message. This is the tool discovery request the Inspector sent when it first connected. Your server responded with all three tool definitions, their descriptions, and their input schemas.
- Find the tools/call message from when you ran create_note. Notice how it includes the tool name and the arguments you provided. The server's response contains the result text.
Why does this matter?
When Claude Desktop connects to your server, it follows this exact same flow. First it sends tools/list to discover what your server can do. Then, based on the user's natural language request, Claude decides which tool to call and sends a tools/call message with the appropriate arguments.
The Inspector gives you firsthand visibility into this protocol. Understanding these messages helps you debug issues when connecting to real AI applications later.
Your server is verified and working. Next up, you will connect it to Claude Desktop and use your tools through natural language.
Connect to Claude Desktop
You have seen the raw protocol messages in the Inspector and confirmed every tool, resource, and prompt works correctly.
Now it is time to connect your server to Claude Desktop so you can interact with your notes through natural language. Claude will discover your tools automatically and decide when to call them based on what you ask.
In this step, get ready to:
- Register your MCP server in the Claude Desktop configuration file.
- Restart Claude Desktop and verify it detects your server.
- Create, search, and retrieve notes through natural language.
Configure Claude Desktop
Claude Desktop looks for a configuration file that tells it which MCP servers to connect to. Each entry specifies a command to launch the server and any arguments it needs.
- First, find the absolute path to your notes_server.py file. In VS Code, right-click notes_server.py in the Explorer sidebar and select Copy Path.
- Paste this path somewhere temporary (like a new VS Code tab) so you have it ready.
Why do I need the absolute path?
Claude Desktop launches your server as a separate process from any directory. A relative path like ./notes_server.py would fail because Claude Desktop does not know where your project folder is. The absolute path ensures it always finds the right file.
- Open the Claude Desktop configuration file by running this command in your VS Code terminal:
code "$env:AppData\Claude\claude_desktop_config.json"
Using CMD instead of PowerShell?
If your VS Code terminal is set to CMD, run this instead:
code %APPDATA%\Claude\claude_desktop_config.json
The file opens in VS Code. It may be empty or contain an empty JSON object.
- Replace everything in the file with the following configuration:
{
"mcpServers": {
"notes": {
"command": "python",
"args": ["[[NOTES_SERVER_PATH=\"C:/Users/YourName/Desktop/notes-mcp-server/notes_server.py\"]]"]
}
}
}
What does this configuration do?
The mcpServers object lists every MCP server Claude Desktop should connect to. Each key (notes) is the server name that appears in the Claude Desktop UI.
The command field tells Claude Desktop to launch python, and args passes your server file as the script to run. Claude Desktop spawns this process on demand using stdio transport.
- Replace the placeholder path with the absolute path you copied earlier. Use forward slashes (/) even on Windows.
- Save claude_desktop_config.json.
Not sure if your path is correct?
Your path should end with notes-mcp-server/notes_server.py and start with your drive letter (e.g. C:/Users/). Make sure there are no extra quotes or backslashes around the path inside the args array.
help me check my claude_desktop_config.json for path errors
Test with Claude
Claude Desktop only reads its configuration file at startup. You need to fully quit and reopen it for the changes to take effect.
- Right-click the Claude icon in your system tray (bottom-right corner of your taskbar).
- Select Quit.
Why not just close the window?
Closing the Claude Desktop window does not actually stop the application. It keeps running in the system tray. You must right-click the tray icon and select Quit to fully shut it down so it re-reads the config on next launch.
- Reopen Claude Desktop by pressing the Windows key, typing Claude, and pressing Enter.
- In the chat input area, click the Add files, connectors, and more plus icon.
- Hover over Connectors to verify that notes appears in the list.
Don't see your notes server listed?
Make sure you fully quit Claude Desktop (not just closed the window) before reopening it.
Open claude_desktop_config.json and check that the JSON is valid. A missing comma, extra bracket, or unmatched quote will silently break the config. Also verify that the path to notes_server.py is absolute and points to a file that exists.
help me troubleshoot why Claude Desktop cannot find my MCP server
Now put your server through its paces with natural language.
- Type the following into the Claude chat and send it: Create a note about learning MCP today.
- Claude will ask permission to use the create_note tool. Click Allow to let it proceed.
- After Claude confirms the note was created, ask: What notes do I have?.
- Ask one more question: Search my notes for MCP.
Watch how Claude discovers which tool to call, passes the right arguments, and translates the raw results into a natural language response.
What is happening under the hood?
Claude Desktop sends a tools/list request to discover your tools (just like the Inspector did). When you ask a question, Claude reads the tool descriptions and decides which one to call.
It then sends a tools/call request with the appropriate arguments. Your server runs the function and returns the result. Claude takes that result and composes a natural language answer for you.
Secret mission
Add Note Statistics and Tag Filtering
Your MCP server works, but a real-world tool needs analytics. Can you add two more tools that give Claude insight into your notes collection and let it filter by tag?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- notes-mcp-server project folder (contains notes_server.py and notes.json).
- Server entry in claude_desktop_config.json.
- mcp Python package (installed via pip).
Keep everything running
No action needed. Your MCP server only runs when Claude Desktop spawns it on demand. There are no background processes and no cost.
- Your notes-mcp-server folder stays on disk with all your notes intact.
- Claude Desktop will continue to discover and use your server whenever you ask it about notes.
- The mcp package and Node.js remain installed for future MCP development.
Pause - I'll come back to this later
No action needed. The server only runs when Claude Desktop invokes it. There is nothing to pause or shut down.
- Your notes and server code remain in your notes-mcp-server folder.
- Claude Desktop will reconnect to your server automatically the next time you open it and ask about notes.
Delete - I don't want to use this again
Remove all project resources and disconnect the server from Claude Desktop.
- Remove the server entry from Claude Desktop by opening the config file:
- Replace the file contents with an empty JSON object and save:
code $env:AppData\Claude\claude_desktop_config.json
Replace the file contents with this and save:
{}
- Delete your project folder by running this command in your terminal:
macOS
rm -rf ~/Desktop/notes-mcp-server
Windows
Remove-Item -Recurse -Force $HOME\Desktop\notes-mcp-server
- Uninstall the mcp Python package by running:
pip uninstall mcp -y
Nice Work!
Nice Work!
You just built a fully working MCP server in Python and connected it to Claude Desktop for real AI-powered note management.
You've learned how to:
- Build an MCP server implementing all three primitives: tools, resources, and prompts.
- Inspect the JSON-RPC tool-calling protocol at the message level using the MCP Inspector.
- Connect a local server to Claude Desktop and use natural language to discover, call, and process tool results.
- Secret Mission: Added statistics and tag filtering tools to iterate on your MCP server.
Ready to quiz yourself?