AI agents (MCP)
graal Data Platform ships an MCP (Model Context Protocol) server: an AI agent connects to it and drives the platform with the rights of the account it carries, not with its own. The server never talks to the database — it translates MCP into the Data Platform’s REST API, carrying the caller’s identity. It is the same server for every compatible MCP client, including the agents of graal Agentic Platform (see The two products).
One clarification that matters before anything else: the platform and the data stay with you, but whatever the tools return to the agent goes through the language model you chose. If that model is hosted by a third party, data leaving is your decision, not a side effect. See Sovereignty and architecture.
The 113 tools
Section titled “The 113 tools”82 read, 31 write, of which 7 destructive tools that are exposed only if the operator decides
so. A server installed without changing anything exposes 106, and none of them deletes anything or
cancels a run (stop_workspace pauses a workspace, files kept: it is reversible). Every tool is backed by an operation the API actually serves: a test refuses a tool that
lands on a disabled feature.
Read (82)
Section titled “Read (82)”| Domain | Tools |
|---|---|
| Projects | list_projects, get_project (and its counters) |
| Jobs and runs | list_jobs, get_job, list_runs, get_run, get_run_logs (by cursor), list_run_files, read_run_file |
| Workflows | list_workflows, get_workflow, get_workflow_run, get_workflow_run_steps (every attempt of every step) |
| Orchestration | get_backfill (progress of a backfill), list_triggers (a project’s triggers, without their secret), list_trigger_events (what a trigger started) |
| Buckets and files | list_bucket_files, read_bucket_file (plain text, binary as base64, bounded) |
| Catalog | list_catalog (layers → databases → tables → fields), get_table, search_catalog (the tables the agent may read) |
| Data connections | list_connections (a project’s databases, S3, SFTP, HTTP APIs, HDFS — the secret is referenced, never returned), test_connection, inspect_connection (tables or files, bounded sample) |
| Lineage and quality | list_lineage_datasets, get_lineage (the graph around a dataset, columns included), get_run_lineage, get_run_quality (the checks report of a run) |
| Data warehouses | list_datawarehouses, get_datawarehouse — never their connection credentials; query_sql (one read-only SQL query, rows capped) |
| Low-code | verify_lowcode (is a DAG valid?), generate_lowcode_code (the Pandas, PySpark… code it produces), list_templates (the template gallery), render_template (a template rendered as a DAG), verify_dag (is a typed DAG valid?), get_preview (the preview of a step) |
| Search | search (projects, jobs, workflows, tables… filtered by the agent’s rights) |
| Workspaces | list_workspaces, get_workspace (and its latest logs) |
| ML experiments and models | list_experiments, get_experiment (and the metrics of its runs), list_experiment_runs, get_experiment_run, list_models |
| Model serving | list_endpoints, get_endpoint, predict (query a served model; nothing is written) |
| Interactive sessions | list_sessions (the open Python kernels and their state) |
| LLM gateway and RAG | llm_chat (a completion charged to a project, personal data masked), list_llm_models, list_rag_indexes, search_documents, ask_documents (an answer that cites its sources) |
| Assistants | generate_pipeline (a low-code pipeline described in natural language, checked by the engine), describe_table (a proposed table description), get_drift (the drift of a served model) |
| Governed agents | list_agents, list_agent_activity, get_approval_status (where an action awaiting approval stands), run_agent (a built-in agent: operations or data engineer) |
| Libraries and secrets | list_libraries, list_secrets — the name of secrets, never their value |
| Infrastructures and runtimes | list_infrastructures, list_runtimes, list_instance_types |
| Quotas, usage, costs | list_quotas, list_usages, get_costs, get_costs_by_project, get_usage_metrics (CPU and memory actually used) |
| Activity | list_activity (sign-ins, notifications, a project’s activity feed), list_audit_actions (the trail of actions, human or agent), export_audit_actions (a whole period) |
| Identity | list_identity_providers (configuration only, never a secret), get_mfa_policy |
| Billing | list_invoices, get_invoice (and its PDF, bounded), list_transactions, get_transaction, list_payment_methods (masked IBAN), get_payment_provider — read only |
verify_lowcode, generate_lowcode_code, render_template and verify_dag send a DAG or a
template to the low-code engine, which validates, translates or renders it without storing
anything: they are reads. So are test_connection and inspect_connection: the platform opens the
connection, lists or reads a sample, and writes nothing. list_audit_actions requires the right
to read the audit trail: an agent without it gets a refusal, not an empty list.
Write (24, plus 7 destructive)
Section titled “Write (24, plus 7 destructive)”| Tool | What it does |
|---|---|
upload_code |
Uploads a code file into the tenant’s libraries |
create_job |
Creates a job in a project (Python, Spark, low-code or bash) |
run_job |
Starts a run, without waiting for its result |
schedule_job |
Sets or replaces a job’s cron schedule |
rerun_run |
Reruns a past run, same parameters, same infrastructure |
run_workflow |
Starts a workflow |
upload_bucket_file |
Drops a file into a bucket |
set_table_description |
Documents a catalog table |
create_workspace |
Starts a Jupyter or VS Code workspace |
start_workspace |
Starts a stopped workspace again, files as they were |
stop_workspace |
Stops a workspace to free its resources, files kept |
comment_activity |
Comments an activity of a project’s feed, signed by the agent’s identity |
follow_resource |
Subscribes the agent’s identity to a resource (project, job, workflow, table…) |
create_session |
Opens an interactive Python session (a kernel, in its own workspace) |
run_session_cell |
Runs a code cell in a session kernel and returns its outputs |
register_model_version |
Registers a model version from a run of the project |
set_model_version_stage |
Moves a model version to a stage (Staging, Production…) or sets its aliases |
schedule_workflow |
Sets or replaces a workflow’s cron schedule (time zone, catch-up of missed occurrences) |
create_backfill |
Replays the schedule of a job or a workflow over a past period (1 to 5 runs at a time) |
create_trigger |
Creates a trigger: end of a run, file dropped into a bucket, or signed webhook |
create_endpoint |
Deploys a model version of the registry behind an endpoint |
scale_endpoint |
Changes the number of replicas of an endpoint (0 stops it, reversible) |
create_connection |
Declares a project’s data connection; its secret is referenced, never sent |
preview_lowcode_step |
Starts the preview of a step of a low-code pipeline (an ephemeral run) |
Destructive, closed by default (GRAAL_MCP_ALLOW_DESTRUCTIVE=1 to open them):
| Tool | What it does |
|---|---|
cancel_run |
Stops a running job run |
cancel_workflow_run |
Stops a running workflow run |
delete_workspace |
Deletes a workspace (files kept only inside it are lost; buckets and tables untouched) |
delete_bucket_file |
Deletes up to 20 files from a bucket (never a directory) |
delete_session |
Deletes an interactive session and its kernel |
cancel_backfill |
Stops a running backfill |
delete_trigger |
Deletes a trigger |
What an agent can destroy, and who decides
Section titled “What an agent can destroy, and who decides”By default, nothing. This is an architectural decision, enforced in the code rather than by convention:
- the seven tools that cancel or delete are registered only if the server’s operator sets
GRAAL_MCP_ALLOW_DESTRUCTIVE=1; without it, the agent does not even see them; - a test refuses any other tool calling a
DELETEoperation; - once open, they are advertised to the protocol with
destructiveHint: true, so that the MCP client asks for confirmation, and they go through the same guard as any write: project rights, notification to the owner; - deleting a project, a job, a workflow, a secret or a table, and removing a schedule, remain human gestures, in the console. No tool does it, option or not.
An agent that gets it wrong creates clutter; it only destroys what you explicitly allowed it to destroy.
No secret value leaves the server
Section titled “No secret value leaves the server”list_secrets returns names, so that a job can reference them; no tool returns the value of a
secret, a token, a warehouse password, a kubeconfig or an S3 key. Every output is reduced to the
useful fields, then passed through a filter that masks any value stored under a secret’s name. The other
way round, no tool receives one: a data connection references its secret by id, and a password
passed as an argument is refused before any call.
One exception, deliberate and tested: create_trigger returns once the signing secret of a
webhook it has just created, with the webhook address and the signing rule (HMAC-SHA256 of the
timestamp and the body). The platform returns it on creation only; no other tool lets it out, and a
lost secret is renewed in the console.
The two authentication modes
Section titled “The two authentication modes”| Mode | Who holds the secret | Where the tenant comes from | What for |
|---|---|---|---|
| Presented token | nobody: the agent brings its own token, the server relays it | the realm carried by the token | normal use, and the only mode allowed off the local machine |
| graal Application | the server, with the credentials of one “graal Application” per tenant | a header or its own configuration | local machine: demo, troubleshooting |
Four guard rails, in this order:
- A token never leaves its tenant. The token’s issuer (
iss) must be exactly<Keycloak URL>/realms/<tenant realm>, checked before the first API call. - The tenant is never a tool parameter. It comes from the session and travels as a header; an agent cannot switch tenants by changing an argument.
- “graal Application” mode refuses to start anywhere but the local interface (
127.0.0.1,::1,localhost): on an exposed port, anyone reaching it would obtain the agent’s rights.GRAAL_MCP_REQUIRE_BEARER=1disables that mode entirely. - The security boundary is the API’s per-project RBAC, not the MCP server. Restricting an agent
to one project through configuration (
GRAAL_MCP_PROJECT_ID) is a convenience; the real rights are those of the account behind the token.
The trail an agent leaves
Section titled “The trail an agent leaves”Every successful write is attributed and notified:
- the created job carries the labels
graal.systems/created-by: agentandgraal.systems/agent: agent-mcp; - the started run carries the initiator
agent-mcp; - a notification goes to the owner of the project touched, plus any configured recipients. A failed notification does not cancel the action: the result handed back to the agent states explicitly whether it went out.
In other words, “who started this run?” has an answer, carried by the run itself.
The platform’s audit log additionally ties every action to its actor, human or agent (see security and governance).
Connecting an agent
Section titled “Connecting an agent”The server listens over Streamable HTTP on 127.0.0.1:7332 by default: entry point
POST /mcp, liveness GET /healthz (which never touches the API). A stdio transport is also
available, for an agent started as a subprocess.
On the MCP client side, the configuration is a few lines:
{ "mcpServers": { "graal": { "type": "http", "url": "http://127.0.0.1:7332/mcp", "headers": { "X-Graal-Tenant": "my-tenant" } } }}In “presented token” mode, add an Authorization: Bearer <token> header: the tenant is then derived
from the token’s realm and X-Graal-Tenant becomes unnecessary.
To list the tools and the REST operation behind each one without starting anything:
graal-mcp --tools.
Configuration
Section titled “Configuration”| Variable | Role |
|---|---|
GRAAL_API_URL |
API root, including the /api/v1 prefix |
GRAAL_KEYCLOAK_URL |
Keycloak’s public origin, used to compute the expected issuer |
GRAAL_MCP_TENANT, GRAAL_MCP_REALM |
The tenant served and its realm |
GRAAL_MCP_CLIENT_ID, GRAAL_MCP_CLIENT_SECRET |
The tenant’s “graal Application” (Application mode) |
GRAAL_MCP_TENANTS |
The same thing as JSON, to serve several tenants |
GRAAL_MCP_PROJECT_ID |
Restricts the agent to one project (convenience, not isolation) |
GRAAL_MCP_NOTIFY_TO |
Extra notification recipients |
GRAAL_MCP_REQUIRE_BEARER |
At 1, only “presented token” mode is accepted |
GRAAL_MCP_ALLOW_DESTRUCTIVE |
At 1, registers the 7 destructive tools (closed by default) |
GRAAL_MCP_HOST, GRAAL_MCP_PORT |
Listening interface and port (default 127.0.0.1:7332) |
None of these variables holds a user secret: the server stores none, and keeps a token in memory only for as long as it is valid.
Errors, and how an agent should read them
Section titled “Errors, and how an agent should read them”Every error is returned in the same shape, with a stable category an agent can act on:
feature_disabled, unauthorized, forbidden, not_found, api_error, auth_error,
config_error, unknown_tenant, out_of_scope, precondition_failed, transport_error.
The one to know is the first: a feature disabled on the installation answers 501 feature_disabled,
never 404, and the message tells the agent not to retry. A 404 therefore really means “this resource
does not exist”, which makes the two cases distinguishable without guessing.
Wiring a client
Section titled “Wiring a client”The server speaks MCP over HTTP. A server exposed beyond the local machine requires the caller’s token. In a compatible client (Claude Code, Claude Desktop, or any other MCP client), the declaration is a few lines:
{ "mcpServers": { "graal": { "type": "http", "url": "https://mcp.example.com/mcp", "headers": { "Authorization": "Bearer <token>" } } }}The token decides who acts:
- an application’s token, that is, a service account’s (see roles and permissions): the agent works for a team, with the roles that application holds on its projects;
- a person’s token: the agent acts on behalf of that person, with exactly their rights.
In both cases, the tenant comes from the token’s realm.
What an agent can do, concretely
Section titled “What an agent can do, concretely”Once wired, the agent has all the tools. A request such as “prepare a job that aggregates regional consumption and schedule it every morning at 6” turns, on its side, into:
list_projects → find the projectupload_code → drop the script into the project's bucketcreate_job → create the job (python, spark, bash or lowcode — nothing else)run_job → start a first executionget_run · get_run_logs → follow it, and read the logs by cursorschedule_job → set the cronEach write pushes a notification to the project owner and lights the « Agent » badge in the console. You do not have to ask the agent what it did: it is written down.
The agent’s guard rails
Section titled “The agent’s guard rails”- Nothing destructive by default. The 7 tools that cancel or delete are registered only with
GRAAL_MCP_ALLOW_DESTRUCTIVE=1, and a test refuses any other tool backed by aDELETEoperation. - A known job type.
create_jobonly acceptspython,spark,bashandlowcode, and refuses before any network call. - No invented value. If the instance type or the identity cannot be derived from what the platform knows, the tool stops and names the human gesture expected rather than forcing the write.
Further reading
Section titled “Further reading”- Installation — what it takes to run the platform
- Sovereignty and architecture
- Security and governance
- The two products — the Agentic Platform’s agents operate the Data Platform through this server
- Public page: AI agents