MCP for tool builders
Build your own MCP-aware client against Dalea.
Connect Claude Desktop is for end users who want their existing LLM to talk to Dalea. This page is different: it's for developers who want to build their own MCP-aware client — a custom agent, an internal tool, a CLI, a desktop assistant — that uses Dalea's tools.
What you get
A bearer-authenticated HTTP MCP endpoint at https://dalea.app/mcp that exposes:
- Documents — list, search, read, create, update, version
- Data — list environments, run saved queries, query result tables, bulk-fetch objects
- Inventory — list containers, items, lots; query by location
- Files — list, get info, fetch via presigned URL
- Search — unified workspace search
- Marketplace — read-only package search
By design, destructive actions are not exposed via MCP — delete, archive,
supersede, and other state-collapsing operations stay in the in-app chat
where a human approves them through a confirmation card. This is a security
property, not an oversight.
Set up
Register an OAuth client
- Settings → Workspaces → OAuth clients → New client
Pick the workspace the client should access. Give it a name ("internal lab agent v1") and a redirect URI for the OAuth callback.
- Pick the role the client will act as
Most agents only need read access. Pick Viewer or Commenter for read-only patterns. Editor or Data Engineer if your agent needs to create documents or records (those write actions go through the in-app chat or your own UI, not via MCP).
- Pick scopes
For an MCP client, you'll typically request:
openid profile email— identityoffline_access— refresh tokensmcp:read— call read-only MCP tools
- Save the client_id and client_secret
Treat the secret like a server-side credential.
Implement the OAuth flow
Standard OAuth 2.1 PKCE for native and CLI clients, server-side flow for web. The exchange:
your client ──/authorize?... ─────────────────────► dalea.app
◄── 302 with ?code=... ───────────────
──/token { code, code_verifier, ... }─►
◄── { access_token, refresh_token } ──
Save the refresh token; use the access token until it returns 401, then refresh.
Make a tool call
Once you have an access token, the MCP endpoint is just JSON-over-HTTP:
import requests
MCP_URL = "https://dalea.app/mcp"
def mcp_call(token, tool, arguments):
resp = requests.post(
MCP_URL,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": tool, "arguments": arguments},
},
)
resp.raise_for_status()
return resp.json()["result"]
# Example: search for protocols
result = mcp_call(token, "search.unified", {"query": "DLA-7", "types": ["document"]})
for hit in result["results"]:
print(hit["title"], hit["url"])
async function mcpCall(token: string, tool: string, args: Record<string, unknown>) {
const resp = await fetch("https://dalea.app/mcp", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: tool, arguments: args },
}),
});
if (!resp.ok) throw new Error(`MCP ${resp.status}`);
return (await resp.json()).result;
}
For most clients you'll use an MCP SDK (the official @modelcontextprotocol/sdk
for Node, or its Python sibling) which handles framing and reconnection for
you.
Discovering available tools
Send tools/list:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
The response enumerates every tool the calling token has access to, with name, description, input schema, and output shape. Cache this for 5 minutes; the tool catalogue is stable but can change between platform releases.
Pagination, filtering, and rate limits
MCP tools follow the same conventions as the REST API:
- List tools accept
limitandcursor. - Filters are explicit arguments, not query strings.
- Rate-limit errors return a standard error response — back off and retry.
A small worked example: a daily PK summary
Goal: every morning, post a Slack message summarising overnight PK results.
The exact tool names below depend on the catalogue your token sees — discover
them with tools/list first. The shape is illustrative:
import os, requests
TOKEN = refresh_or_use_cached_token() # OAuth helper
# 1. Discover available tools
tools = mcp_list(TOKEN)
# Look for tools related to saved queries; pick the one that lists them.
list_tool = next(t for t in tools if "list" in t["name"] and "quer" in t["name"])
run_tool = next(t for t in tools if "run" in t["name"] and "quer" in t["name"])
# 2. Find the saved query for "DLA-7 PK timecourse by dose group"
queries = mcp_call(TOKEN, list_tool["name"], {"name_contains": "DLA-7 PK timecourse"})
qid = queries["items"][0]["id"]
# 3. Run it for the latest data
data = mcp_call(TOKEN, run_tool["name"], {"query_id": qid})
# 4. Format and post to Slack
summary = format_pk_table(data["rows"]) # your own helper
requests.post(os.environ["SLACK_WEBHOOK"], json={"text": summary})
Schedule this with cron, GitHub Actions, or your scheduler of choice. No human in the loop for the MCP call; the data tools are read-only so MCP is the right surface.
What MCP isn't
- It isn't a webhook. MCP is request/response. Event-driven push notifications (e.g. "tell me when a result batch closes") aren't yet available — for now, integrators poll the REST API on a schedule. Native webhooks are on the roadmap.
- It isn't for destructive actions. No deletes, no closes, no signatures. Those go through the in-app chat.
- It isn't optimised for bulk transfer. For exporting 100 000 rows, use the REST API with cursor pagination.
Tips
The official MCP SDKs handle the JSON-RPC envelope, retries and reconnection. Save yourself the boilerplate and use them for any non-trivial client.
An MCP client doesn't need the same role as the user who registered it. A read-only Slack notifier should be Viewer; an analysis agent that ingests data into a customer-built reporting tool should be Commenter. Match the principle of least privilege.