Make your first MCP call
In this tutorial you call Drum’s MCP server by hand, with curl, the way an AI client does. You will see the messages a client sends, what the server answers, and how the tools fit together. This is for developers who want to build on the server or understand what their AI client is doing.
It takes about 15 minutes. You change no data.
What you need
- A Drum user with
api_readon its role, or an account admin user - A terminal with
curl.jqmakes the answers easier to read, but is optional
Step 1: Create a token
- In Drum, open your user menu and select Profile, then API.
- Create a token for your account. Under Toolsets, leave My work selected. (See Create an API token for the full steps.)
- Copy the token, and put it in your terminal:
export DRUM_API_TOKEN="paste-your-token-here"
export DRUM_MCP="https://app.getdrum.com/api/v1/mcp"
Step 2: Start the conversation
An MCP client starts with initialize. It says which protocol version it speaks and who it is:
curl -s -X POST "$DRUM_MCP" \
-H "Authorization: Bearer $DRUM_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "curl-tutorial", "version": "1.0"}
}
}' | jq .
The answer looks like this, shortened:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {"tools": {"listChanged": true}, "prompts": {"listChanged": true}, "resources": {"listChanged": true}, "logging": {}},
"serverInfo": {"name": "drum", "version": "1.0.0"},
"instructions": "Drum is practice management software for project firms. ..."
}
}
Read instructions. This is the text an AI client gives the model before it uses any tool. It says what Drum is, what your toolsets do, and the rules, such as “money is integer cents” and “get each id from search_records”.
A client then sends a notification to say it is ready. The server answers 202 with no body:
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$DRUM_MCP" \
-H "Authorization: Bearer $DRUM_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "method": "notifications/initialized"}'
Step 3: List the tools
curl -s -X POST "$DRUM_MCP" \
-H "Authorization: Bearer $DRUM_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}' \
| jq '.result.tools[] | {name, readOnly: .annotations.readOnlyHint}'
You see the tools of the My work toolset that your role allows, such as search_records, list_projects, get_project and create_tracked_time. Each has a description and an input schema. A model reads these to decide which tool to call.
Step 4: Find a project
Every other tool takes ids, and search_records is how you find them. Search for one of your projects by a word in its name. Change Riverside to a word from one of yours:
curl -s -X POST "$DRUM_MCP" \
-H "Authorization: Bearer $DRUM_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "search_records",
"arguments": {"query": "Riverside", "kinds": ["project"]}
}
}' | jq '.result.structuredContent'
{
"rows": [
{"kind": "project", "id": 20488, "name": "Riverside fit-out", "parent": null, "status": "In progress", "date": "2026-08-01", "match": "exact"}
],
"count": 1,
"limit": 25,
"offset": 0,
"has_more": false
}
Note the project’s id. match is exact here. A row with approximate is only similar to what you searched for, so a client should check with the user before it uses that id.
Step 5: Read the project
Call get_project with the id you found:
curl -s -X POST "$DRUM_MCP" \
-H "Authorization: Bearer $DRUM_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "get_project",
"arguments": {"project_id": 20488, "include": ["deliverables", "tasks"]}
}
}' | jq '.result.structuredContent | {name, deliverables: [.deliverables[] | {name, tasks: [.tasks[]?.name]}]}'
You get the project with its deliverables and their tasks. The task ids here are what create_tracked_time takes.
Step 6: See a tool error
Call get_project with an id that does not exist:
curl -s -X POST "$DRUM_MCP" \
-H "Authorization: Bearer $DRUM_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "get_project", "arguments": {"project_id": 1}}}' \
| jq '.result | {isError, error: .structuredContent.error}'
The call does not fail at the HTTP or JSON-RPC level. It returns a normal result with isError: true and an error with a code, such as not_found, and a message that tells the model what to try next. A project you are not allowed to see gives the same answer, so a hidden record looks the same as a missing one.
What you have learned
- A client talks to Drum with JSON-RPC messages over HTTPS, one in each request, with your token as a bearer token.
initializegives the model its instructions, andtools/listgives it the tools your toolsets and role allow.search_recordsturns names into ids, and the other tools take those ids.- A refused call is a normal result with
isError: trueand an error code.
Next steps
- Server and protocol for every method and rule
- My work tools for each tool’s inputs
- Build an agent on the Drum MCP server to put a model in the loop