Building an SAP BTP Agent Architecture from Scratch built a CAP retrieval service and Flask orchestrator on SAP BTP. This builds on top of that: an MCP server exposing a search_documents(query) tool, so Claude can call into the CAP retrieval service directly, through Claude Desktop or a claude.ai connector.
Architecture
The backend is a CAP service on Cloud Foundry, btp-agent-retrieval-srv, deployed 2026-07-28, exposing a Documents entity and a search action backed by HANA Cloud full-text search. Six documents live in it — four about Philip K. Dick, two about R2-D2. A separate Flask app, btp-agent-orchestrator, sits in front of it: takes a natural-language question, extracts search keywords, calls the CAP search action, and asks Claude to generate a grounded answer from whatever comes back.
Three things can trigger a search here: a standalone HTML page (ask.html), Claude Desktop, and a claude.ai connector. They only take two different backend paths, though — ask.html goes through the Flask orchestrator, while Claude Desktop and the connector both go through this MCP server, just over different transports, covered below. Either way, requests land on the same btp-agent-retrieval-srv CAP service and the same HANA data.
stdio or HTTP
MCP (Model Context Protocol) is what lets an AI assistant call out to an external tool — here, the search_documents tool this server exposes on top of the CAP service described above. MCP supports more than one delivery method for getting data between a client and a server — call it a transport, but it’s really just the channel the two sides use to talk, not what they’re saying. That matters here because Claude Desktop and claude.ai are two very different kinds of client.
Claude Desktop runs on the same machine as the MCP server. It starts mcp_server.py itself, as a subprocess, and talks to it directly by writing to its input and reading its output — no network connection involved. This delivery method is called stdio.
claude.ai is different: it runs in Anthropic’s cloud, not on this machine, so it can’t launch anything locally. It can only reach a server with its own address on the internet — one that’s actually listening on an open port. This delivery method is HTTP.
That’s the whole distinction: stdio for a client running alongside the server, HTTP for a client running somewhere else entirely. A claude.ai “connector” is just a registered HTTP address that claude.ai knows to talk MCP to. Same script, same tool, two different delivery methods for reaching it — which is why getting Desktop working and getting the claude.ai connector working turned out to be two separate jobs, not one.
Running it locally first
python3 mcp_server.py with the default stdio transport (mcp.run()) exits immediately when run directly in a terminal:
python3 mcp_server.py
echo $?
0
Exit code 0, no error. Stdio transport blocks and stays open only when connected to another process reading and writing over stdin/stdout — the way Claude Desktop launches it as a subprocess. Run directly from a terminal, stdin hits EOF straight away, so the process exits cleanly with nothing to show for it.
To confirm the server itself was actually working before worrying about Desktop, temporarily switched the transport:
mcp.run(transport=”streamable-http”)
That stayed running and printed a Uvicorn startup line on http://127.0.0.1:8000.
The real validation for the stdio mode Desktop needs came from MCP Inspector, which spawns the server itself as a subprocess over stdio, the same way Desktop does:
npx @modelcontextprotocol/inspector /Library/Frameworks/Python.framework/Versions/3.14/bin/python3 mcp_server.py
That opened a web UI at localhost:6274. An earlier session reported it showed search_documents as an available, callable tool there — that specific detail wasn’t independently confirmed within this work, only that Inspector came up and connected. With that reported as working, the transport was reverted to bare mcp.run() for the stdio mode Desktop expects.
Config pointing at the wrong path
Claude Desktop showed btp-agent-retrieval: Server disconnected. The per-server log at ~/Library/Logs/Claude/mcp-server-btp-agent-retrieval.log:
/Library/Frameworks/Python.framework/Versions/3.14/Resources/Python.app/Contents/MacOS/Python: can’t open file ‘/Users/neilaspin/Documents/GitHub/btp-agent-poc/mcp-server/mcp_server.py’: [Errno 2] No such file or directory
The script lived a level deeper, at orchestrator/mcp-server/mcp_server.py. claude_desktop_config.json had never pointed at the real location. Fixed the args entry to match.
The same config file uses a full interpreter path rather than a bare python3 — /Library/Frameworks/Python.framework/Versions/3.14/bin/python3 — since Desktop’s launch environment, a minimal PATH with no shell profile, may not resolve a bare command name to the right interpreter. This was already in place by the time the config was checked during this work, so whatever prompted it happened earlier, not something diagnosed here. The relevant part of claude_desktop_config.json:
“btp-agent-retrieval”: {
“command”: “/Library/Frameworks/Python.framework/Versions/3.14/bin/python3”,
“args”: [“/Users/neilaspin/Documents/GitHub/btp-agent-poc/orchestrator/mcp-server/mcp_server.py”]
}
Restarting Desktop and a stale process
Desktop only re-reads claude_desktop_config.json on launch, so a config edit means quitting and reopening the app. Two restarts were tried early on, before anything had been diagnosed — hoping a fresh launch alone would clear the disconnect. The “Server disconnected” state was unchanged both times.
Checking which process was actually behind Desktop’s connection showed why: the MCP server child process had started at 9:53am — before any of the day’s fixes to the script or config — and was still running, still pointed at the old script path and the old URL. Quitting and reopening the Desktop app hadn’t killed that process; it had kept running independently of the app restarts. A separate process was also running on port 8000, left over from testing the HTTP transport. Killed both directly, then restarted Desktop again.
Hardcoded URL, wrong from the start
A search_documents call from a claude.ai connector:
404 Client Error: Not Found for url: https://cap-service.cfapps.ap10.hana.ondemand.com/odata/v4/retrieval/search
cap-service was never the deployed app’s name — manifest.yml shows it as btp-agent-retrieval-srv — and the service’s @(path: ‘/retrieval’) annotation means it doesn’t sit under /odata/v4/ at all. CAP_SERVICE_URL in mcp_server.py had both the wrong host and the wrong path:
CAP_SERVICE_URL = “https://cap-service.cfapps.ap10.hana.ondemand.com/odata/v4/retrieval”
Edited that line directly in mcp_server.py: the host changed from cap-service to btp-agent-retrieval-srv, and /odata/v4/retrieval changed to just /retrieval, matching the service’s actual path annotation.
CAP_SERVICE_URL = “https://btp-agent-retrieval-srv.cfapps.ap10.hana.ondemand.com/retrieval”
HANA instance stopped
curl https://btp-agent-retrieval-srv.cfapps.ap10.hana.ondemand.com/retrieval/Documents
{“error”:{“code”:”503″,”message”:”Service Unavailable”}}
cf app btp-agent-retrieval-srv showed the app running normally, 1/1 instances, healthy. The app’s own logs:
“msg”:”Could not establish connection for tenant “undefined” due to error: HANA Database instance is stopped”
Free-tier HANA instances auto-stop from inactivity — the same behavior hit during the original build of this project, this time on a live, already-deployed app rather than at provisioning time.
cf update-service HANA_TRAINING_FREE -c ‘{“data”: {“serviceStopped”: false}}’
That command returns immediately; the resume itself runs asynchronously. cf service HANA_TRAINING_FREE reported update in progress for several minutes before update succeeded.
curl https://btp-agent-retrieval-srv.cfapps.ap10.hana.ondemand.com/retrieval/Documents
{“@odata.context”:”$metadata#Documents”,”value”:[{“ID”:”…”,”title”:”Philip K. Dick – Recurring Themes”, …}, …]}
That confirms the CAP service and HANA are both up. The actual point of this project is the MCP layer on top, so the real test is calling search_documents itself rather than the raw endpoint:
search_documents(query=”philip k dick r2d2″)
{
“@odata.context”: “$metadata#Documents”,
“value”: [
{“ID”: “a9728627-…”, “title”: “Philip K. Dick – Biography”, …},
{“ID”: “cef952b4-…”, “title”: “On Writers, Speed, and Philip K. Dick”, …},
{“ID”: “5ce53328-…”, “title”: “Philip K. Dick – Major Works”, …},
{“ID”: “261dda86-…”, “title”: “Philip K. Dick – Recurring Themes”, …},
{“ID”: “2f8040d7-…”, “title”: “R2D2 history”, …},
{“ID”: “a9e89909-…”, “title”: “R2D2 dialogue”, …}
]
}
All six seeded documents came back, through the MCP tool, not a direct API call.
Everything here is the stdio path — Claude Desktop launching the script locally. A working claude.ai connector needs the HTTP transport instead: the same server running as its own standalone process with a real URL, deployed and registered separately.
Building an SAP BTP Agent Architecture from Scratch built a CAP retrieval service and Flask orchestrator on SAP BTP. This builds on top of that: an MCP server exposing a search_documents(query) tool, so Claude can call into the CAP retrieval service directly, through Claude Desktop or a claude.ai connector.ArchitectureThe backend is a CAP service on Cloud Foundry, btp-agent-retrieval-srv, deployed 2026-07-28, exposing a Documents entity and a search action backed by HANA Cloud full-text search. Six documents live in it — four about Philip K. Dick, two about R2-D2. A separate Flask app, btp-agent-orchestrator, sits in front of it: takes a natural-language question, extracts search keywords, calls the CAP search action, and asks Claude to generate a grounded answer from whatever comes back.Three things can trigger a search here: a standalone HTML page (ask.html), Claude Desktop, and a claude.ai connector. They only take two different backend paths, though — ask.html goes through the Flask orchestrator, while Claude Desktop and the connector both go through this MCP server, just over different transports, covered below. Either way, requests land on the same btp-agent-retrieval-srv CAP service and the same HANA data.stdio or HTTPMCP (Model Context Protocol) is what lets an AI assistant call out to an external tool — here, the search_documents tool this server exposes on top of the CAP service described above. MCP supports more than one delivery method for getting data between a client and a server — call it a transport, but it’s really just the channel the two sides use to talk, not what they’re saying. That matters here because Claude Desktop and claude.ai are two very different kinds of client.Claude Desktop runs on the same machine as the MCP server. It starts mcp_server.py itself, as a subprocess, and talks to it directly by writing to its input and reading its output — no network connection involved. This delivery method is called stdio.claude.ai is different: it runs in Anthropic’s cloud, not on this machine, so it can’t launch anything locally. It can only reach a server with its own address on the internet — one that’s actually listening on an open port. This delivery method is HTTP.That’s the whole distinction: stdio for a client running alongside the server, HTTP for a client running somewhere else entirely. A claude.ai “connector” is just a registered HTTP address that claude.ai knows to talk MCP to. Same script, same tool, two different delivery methods for reaching it — which is why getting Desktop working and getting the claude.ai connector working turned out to be two separate jobs, not one.Running it locally firstpython3 mcp_server.py with the default stdio transport (mcp.run()) exits immediately when run directly in a terminal:python3 mcp_server.py
echo $?
0Exit code 0, no error. Stdio transport blocks and stays open only when connected to another process reading and writing over stdin/stdout — the way Claude Desktop launches it as a subprocess. Run directly from a terminal, stdin hits EOF straight away, so the process exits cleanly with nothing to show for it.To confirm the server itself was actually working before worrying about Desktop, temporarily switched the transport:mcp.run(transport=”streamable-http”)That stayed running and printed a Uvicorn startup line on http://127.0.0.1:8000.The real validation for the stdio mode Desktop needs came from MCP Inspector, which spawns the server itself as a subprocess over stdio, the same way Desktop does:npx @modelcontextprotocol/inspector /Library/Frameworks/Python.framework/Versions/3.14/bin/python3 mcp_server.pyThat opened a web UI at localhost:6274. An earlier session reported it showed search_documents as an available, callable tool there — that specific detail wasn’t independently confirmed within this work, only that Inspector came up and connected. With that reported as working, the transport was reverted to bare mcp.run() for the stdio mode Desktop expects.Config pointing at the wrong pathClaude Desktop showed btp-agent-retrieval: Server disconnected. The per-server log at ~/Library/Logs/Claude/mcp-server-btp-agent-retrieval.log:/Library/Frameworks/Python.framework/Versions/3.14/Resources/Python.app/Contents/MacOS/Python: can’t open file ‘/Users/neilaspin/Documents/GitHub/btp-agent-poc/mcp-server/mcp_server.py’: [Errno 2] No such file or directoryThe script lived a level deeper, at orchestrator/mcp-server/mcp_server.py. claude_desktop_config.json had never pointed at the real location. Fixed the args entry to match.The same config file uses a full interpreter path rather than a bare python3 — /Library/Frameworks/Python.framework/Versions/3.14/bin/python3 — since Desktop’s launch environment, a minimal PATH with no shell profile, may not resolve a bare command name to the right interpreter. This was already in place by the time the config was checked during this work, so whatever prompted it happened earlier, not something diagnosed here. The relevant part of claude_desktop_config.json:”btp-agent-retrieval”: {
“command”: “/Library/Frameworks/Python.framework/Versions/3.14/bin/python3”,
“args”: [“/Users/neilaspin/Documents/GitHub/btp-agent-poc/orchestrator/mcp-server/mcp_server.py”]
}Restarting Desktop and a stale processDesktop only re-reads claude_desktop_config.json on launch, so a config edit means quitting and reopening the app. Two restarts were tried early on, before anything had been diagnosed — hoping a fresh launch alone would clear the disconnect. The “Server disconnected” state was unchanged both times.Checking which process was actually behind Desktop’s connection showed why: the MCP server child process had started at 9:53am — before any of the day’s fixes to the script or config — and was still running, still pointed at the old script path and the old URL. Quitting and reopening the Desktop app hadn’t killed that process; it had kept running independently of the app restarts. A separate process was also running on port 8000, left over from testing the HTTP transport. Killed both directly, then restarted Desktop again.Hardcoded URL, wrong from the startA search_documents call from a claude.ai connector:404 Client Error: Not Found for url: https://cap-service.cfapps.ap10.hana.ondemand.com/odata/v4/retrieval/searchcap-service was never the deployed app’s name — manifest.yml shows it as btp-agent-retrieval-srv — and the service’s @(path: ‘/retrieval’) annotation means it doesn’t sit under /odata/v4/ at all. CAP_SERVICE_URL in mcp_server.py had both the wrong host and the wrong path:CAP_SERVICE_URL = “https://cap-service.cfapps.ap10.hana.ondemand.com/odata/v4/retrieval”Edited that line directly in mcp_server.py: the host changed from cap-service to btp-agent-retrieval-srv, and /odata/v4/retrieval changed to just /retrieval, matching the service’s actual path annotation.CAP_SERVICE_URL = “https://btp-agent-retrieval-srv.cfapps.ap10.hana.ondemand.com/retrieval”HANA instance stoppedcurl https://btp-agent-retrieval-srv.cfapps.ap10.hana.ondemand.com/retrieval/Documents
{“error”:{“code”:”503″,”message”:”Service Unavailable”}}cf app btp-agent-retrieval-srv showed the app running normally, 1/1 instances, healthy. The app’s own logs:”msg”:”Could not establish connection for tenant “undefined” due to error: HANA Database instance is stopped”Free-tier HANA instances auto-stop from inactivity — the same behavior hit during the original build of this project, this time on a live, already-deployed app rather than at provisioning time.cf update-service HANA_TRAINING_FREE -c ‘{“data”: {“serviceStopped”: false}}’That command returns immediately; the resume itself runs asynchronously. cf service HANA_TRAINING_FREE reported update in progress for several minutes before update succeeded.curl https://btp-agent-retrieval-srv.cfapps.ap10.hana.ondemand.com/retrieval/Documents
{“@odata.context”:”$metadata#Documents”,”value”:[{“ID”:”…”,”title”:”Philip K. Dick – Recurring Themes”, …}, …]}That confirms the CAP service and HANA are both up. The actual point of this project is the MCP layer on top, so the real test is calling search_documents itself rather than the raw endpoint:search_documents(query=”philip k dick r2d2″)
{
“@odata.context”: “$metadata#Documents”,
“value”: [
{“ID”: “a9728627-…”, “title”: “Philip K. Dick – Biography”, …},
{“ID”: “cef952b4-…”, “title”: “On Writers, Speed, and Philip K. Dick”, …},
{“ID”: “5ce53328-…”, “title”: “Philip K. Dick – Major Works”, …},
{“ID”: “261dda86-…”, “title”: “Philip K. Dick – Recurring Themes”, …},
{“ID”: “2f8040d7-…”, “title”: “R2D2 history”, …},
{“ID”: “a9e89909-…”, “title”: “R2D2 dialogue”, …}
]
}All six seeded documents came back, through the MCP tool, not a direct API call.Everything here is the stdio path — Claude Desktop launching the script locally. A working claude.ai connector needs the HTTP transport instead: the same server running as its own standalone process with a real URL, deployed and registered separately. Read More Technology Blog Posts by Members articles
#SAP
#SAPTechnologyblog