Lesson 12 of 30, Module 2: Your First Server. Your own server edits real documents in your Drive folder.
What you can do after this lesson
An MCP tool is not new code. It is an ordinary function with one decorator line above it and a plain-words docstring under its name. The server builds the name, description and input rule from what you were already writing, so you describe a capability once and any host can read it.
The problem we inherited
You install the official Google Drive server and Claude reads every document and changes none of them.
The 5 steps this lesson runs
- Write the plain Python function
- Put the @mcp line above it
- Write the one-line docstring
- Restart Claude Desktop
- Ask Claude to use it
Code in this lesson
Every block the lesson shows on screen, in the order it appears.
1. Write the plain Python function
2. Put the @mcp line above it
3. Write the one-line docstring
4. Restart Claude Desktop
5. Ask Claude to use it
import io
import os
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseUpload
from mcp.server.mcpserver import MCPServer
FOLDER = os.environ["SCRIBE_FOLDER_ID"]
KEY = os.environ.get("SCRIBE_KEY", "key.json")
SCOPES = ["https://www.googleapis.com/auth/drive"]
creds = service_account.Credentials.from_service_account_file(
KEY, scopes=SCOPES)
drive = build("drive", "v3", credentials=creds,
cache_discovery=False)
mcp = MCPServer("scribe")
def _find(name: str) -> str:
q = (f"name = '{name}' and '{FOLDER}' in parents"
" and trashed = false")
found = drive.files().list(q=q, fields="files(id)").execute()
if not found["files"]:
raise ValueError(
f"There is no draft called {name} in your folder.")
return found["files"][0]["id"]
def _text(file_id: str) -> str:
raw = drive.files().get_media(fileId=file_id).execute()
return raw.decode()
def read_doc(name: str) -> str:
return _text(_find(name))
@mcp.tool()
def read_doc(name: str) -> str:
return _text(_find(name))
@mcp.tool()
def read_doc(name: str) -> str:
"""Read one draft from the Scribe folder in Google Drive."""
return _text(_find(name))
export SCRIBE_FOLDER_ID="paste your folder id here"
export SCRIBE_KEY="key.json"
uv run what_can_it_do.py
python3 what_can_it_do.py
TOOLS
read_doc
Read one draft from the Scribe folder in Google Drive.
input: name (string, required)
older articles inputSchema
the package today input_schema
@mcp.tool()
def edit_doc(name: str, find: str, replace: str) -> str:
"""Replace some text inside one draft in the Scribe folder."""
file_id = _find(name)
text = _text(file_id)
if find not in text:
return f"That text is not in {name}. Nothing changed."
new = text.replace(find, replace).encode()
body = MediaIoBaseUpload(io.BytesIO(new),
mimetype="text/markdown")
drive.files().update(fileId=file_id,
media_body=body).execute()
return f"Changed {text.count(find)} spot(s) in {name}."
if __name__ == "__main__":
mcp.run()
TOOLS
read_doc
Read one draft from the Scribe folder in Google Drive.
input: name (string, required)
edit_doc
Replace some text inside one draft in the Scribe folder.
input: name (string, required)
input: find (string, required)
input: replace (string, required)
"scribe": {
"command": "/Users/you/scribe/.venv/bin/python",
"args": ["/Users/you/scribe/scribe_server.py"],
"env": {
"SCRIBE_FOLDER_ID": "paste your folder id here",
"SCRIBE_KEY": "/Users/you/scribe/key.json"
}
}
python3 verify_claude_config.py
RESULT: your config worked. Claude is running every server in it.
Use scribe to fix the typo in draft.md.
Downloads
- Module 2 checkpoint (scribe-checkpoint-m2.zip): working code as the module leaves it, so a broken session costs you nothing
- The course GitHub repo: every checkpoint, the README and the full lesson list
Prefer to read?
The written version of this part of the course is on Substack: https://genaiunplugged.substack.com/p/how-to-build-an-mcp-server-and-connect.
Lesson transcript
Lesson 12: a server that can edit
Today Claude fixes a typo in a real document in my Drive, and I never open the document.
Hello, and welcome to lesson 12 of the MCP Masterclass. Last lesson we met a server that reads everything and changes nothing. Today you write your own server, and yours can edit. And we start with our map, as always.
6 lit boxes, and the hole in the middle. By the end of this lesson that hole is gone. The server box lights up, and 2 small boxes light up beside it.
The finished thing
So let me show you the finished thing first.
On screen: Claude Desktop fixes the typo in draft.md through Scribe. Then draft.md open in Drive, with the typo gone.
I ask Claude to fix the typo in my draft. Claude asks my permission, I say yes, and the document in my Drive changes.
I did not copy anything, and I did not paste anything back. Now let us go back and build it.
The 5 steps
Every build in this course follows the same 5 steps. Here they are.
1. Write the plain Python function
2. Put the @mcp line above it
3. Write the one-line docstring
4. Restart Claude Desktop
5. Ask Claude to use it
Write the plain Python function. Put the @mcp line above it. Write the one line docstring. Restart Claude Desktop. Ask Claude to use it.
You will see this same list before every build. Today we go through it slowly, and I explain each step as we reach it.
Making the server file
Before step 1, we need a file for the steps to live in.
Open your scribe folder, and make a new file. Call it scribe_server.py.
Put this at the top.
import io
import os
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseUpload
from mcp.server.mcpserver import MCPServer
FOLDER = os.environ["SCRIBE_FOLDER_ID"]
KEY = os.environ.get("SCRIBE_KEY", "key.json")
SCOPES = ["https://www.googleapis.com/auth/drive"]
creds = service_account.Credentials.from_service_account_file(
KEY, scopes=SCOPES)
drive = build("drive", "v3", credentials=creds,
cache_discovery=False)
mcp = MCPServer("scribe")
Well, most of those lines are old friends. You wrote them in lesson 5, inside draft.py.
There are 3 changes, and each one is small.
The first change. The folder id and the key no longer sit inside the code. The file reads them from its surroundings when it starts.
I am deliberate about this, because your folder id and your key belong to you. With this change you can hand the file to a friend, and you give away nothing.
The second change is one new import. It brings in the server class from the MCP package.
And the third change is the very last line. That line makes a server, and it names the server scribe.
A server is nothing but a small program that offers jobs to your AI. So far, ours offers none.
Next come 2 small helpers. These are from lesson 5 as well, and I have only split them apart.
def _find(name: str) -> str:
q = (f"name = '{name}' and '{FOLDER}' in parents"
" and trashed = false")
found = drive.files().list(q=q, fields="files(id)").execute()
if not found["files"]:
raise ValueError(
f"There is no draft called {name} in your folder.")
return found["files"][0]["id"]
def _text(file_id: str) -> str:
raw = drive.files().get_media(fileId=file_id).execute()
return raw.decode()
The first helper finds a draft by its name. Notice that it names the folder in the search. That is trap number 4 from lesson 4.
And if there is no such draft, it says so in a plain sentence.
The second helper fetches the words inside a draft.
Steps 1 to 3
Okay. Now we are ready for the 5 steps.
Step 1. Write the plain Python function.
def read_doc(name: str) -> str:
return _text(_find(name))
That is 2 lines. It takes the name of a draft, and it gives back the words.
I want you to look at it closely, because there is nothing about MCP in it. It is an ordinary Python function, and you could call it from any script.
Step 2. Put the @mcp line above it.
@mcp.tool()
def read_doc(name: str) -> str:
return _text(_find(name))
One line. That line hands your function to the server, and your function becomes a tool.
A tool is nothing but a job the server can do when your AI asks for it.
Step 3. Write the one line docstring.
@mcp.tool()
def read_doc(name: str) -> str:
"""Read one draft from the Scribe folder in Google Drive."""
return _text(_find(name))
A docstring is nothing but a sentence in plain words, sitting just under the function name.
And you are writing this sentence for Claude. Claude reads it, and Claude uses it to decide when to pick this tool.
So write it the way you would brief a new colleague. Say what the job does, and keep it plain.
A tool describes itself
Now, do you remember reason number 3 from lesson 8? A tool describes itself, and a script cannot.
This is where that pays off. So let us ask the server what it offers.
Your module 2 checkpoint has a small script for this. Set your folder id and your key, and run it.
export SCRIBE_FOLDER_ID="paste your folder id here"
export SCRIBE_KEY="key.json"
uv run what_can_it_do.py
Or, if you chose pip in lesson 3, run it with python3.
python3 what_can_it_do.py
Windows users, your lines are in the checkpoint notes.
And here is the answer that comes back.
TOOLS
read_doc
Read one draft from the Scribe folder in Google Drive.
input: name (string, required)
Now look at what you did not write.
The name came from your function name. The description came from your docstring.
And the input came from your type hint. You wrote the word name, a colon, and the letters s t r. The server turned that into a rule. The input is called name, it must be text, and it must be there.
Nobody wrote that rule by hand. You described your tool once, in the place you were writing code anyway.
That is reason number 3, paid in full.
One warning while we are here, because old sample code is everywhere.
That input rule has a name, and older articles spell the name differently. They join 2 words with a capital letter in the middle. The package you installed puts a underscore between them.
older articles inputSchema
the package today input_schema
Copy code from an older article, and it fails on that exact word. So when a sample breaks, check its date before you check your own work.
The edit tool
Now the second tool, and this is the one the official server does not have.
Same 3 steps. The function, the @mcp line, and the docstring.
@mcp.tool()
def edit_doc(name: str, find: str, replace: str) -> str:
"""Replace some text inside one draft in the Scribe folder."""
file_id = _find(name)
text = _text(file_id)
if find not in text:
return f"That text is not in {name}. Nothing changed."
new = text.replace(find, replace).encode()
body = MediaIoBaseUpload(io.BytesIO(new),
mimetype="text/markdown")
drive.files().update(fileId=file_id,
media_body=body).execute()
return f"Changed {text.count(find)} spot(s) in {name}."
Let us walk through it from the top.
It takes 3 inputs. The name of the draft, the words to find, and the words to put in their place.
It finds the draft, and it fetches the words. Then it checks that your text is really in there.
If the text is missing, it says so, and it changes nothing. I want a tool to be honest when nothing happened.
If the text is there, it swaps it, and it sends the new words up to Drive. Then it tells Claude how many spots it changed.
One more piece, and the file is finished. Put this at the very bottom.
if __name__ == "__main__":
mcp.run()
That starts the server, which then sits quietly and waits for a host to talk to it.
Run the checker again, and you now see both tools.
TOOLS
read_doc
Read one draft from the Scribe folder in Google Drive.
input: name (string, required)
edit_doc
Replace some text inside one draft in the Scribe folder.
input: name (string, required)
input: find (string, required)
input: replace (string, required)
Restart Claude Desktop
Step 4. Restart Claude Desktop.
But first, Claude Desktop has to know that your server exists. Open the settings file from lesson 3, and add scribe next to the 2 servers from lesson 6.
"scribe": {
"command": "/Users/you/scribe/.venv/bin/python",
"args": ["/Users/you/scribe/scribe_server.py"],
"env": {
"SCRIBE_FOLDER_ID": "paste your folder id here",
"SCRIBE_KEY": "/Users/you/scribe/key.json"
}
}
Let us read that entry together.
The command is the Python that lives inside your scribe folder, and the first argument is your server file.
And the last part hands over your folder id and your key. Those are the surroundings your file reads when it starts.
Remember the 2 rules from lesson 3. Every path is a full path, from the very top of your disk. And you quit Claude Desktop properly, then you open it again.
Windows users, your Python sits in a slightly different place. The exact line is in the checkpoint notes.
Then run the checker from your module 1 checkpoint.
python3 verify_claude_config.py
You want to see this at the bottom.
RESULT: your config worked. Claude is running every server in it.
Ask Claude to use it
Step 5. Ask Claude to use it.
Remember what we learned in lesson 6. Having a tool and reaching for a tool are 2 different things. So name your server when you ask.
Use scribe to fix the typo in draft.md.
On screen: Claude Desktop stops and asks permission to run edit_doc, showing the 3 inputs it is about to send.
Claude Desktop stops, and it asks you first. It names the tool it wants to run, and it shows you the inputs.
That asking is reason number 4 from lesson 9. Say yes.
On screen: the tool answers "Changed 1 spot(s) in draft.md." Then draft.md open in Drive, with the typo gone.
And there it is. Claude read the draft, found the typo, and changed the real document.
What you built
Now stop for a moment, because I do not want you to miss what just happened.
You built the thing the official Google Drive server cannot do.
So what did all of that cost you?
Your whole server is 1 small file. And the part that is truly new is 2 short functions. Everything around them is setup you had already written in lesson 5.
From today, fixing a draft costs you 1 sentence. You do not open the document, you do not copy, and you do not paste. And no stale copy sits in a chat.
And every app that speaks MCP can use this server. You wrote no glue for any of them.
That was the slow walk through the 5 steps. It gets faster every time, and I will point it out when it does.
So where does our map stand at the end of this lesson?
The hole is gone. Your Scribe server is lit, and so are read_doc and edit_doc beside it. That makes 9 lit boxes.
Next lesson we will prove that the real document changed, and then we will try to break it.
Bye now, and I will see you in the next lesson.