Skip to content
FRBook a demo

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.

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.

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.

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 DELETE operation;
  • 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.

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.

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:

  1. 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.
  2. 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.
  3. “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=1 disables that mode entirely.
  4. 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.

Every successful write is attributed and notified:

  • the created job carries the labels graal.systems/created-by: agent and graal.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).

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.

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.

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.

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.

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 project
upload_code → drop the script into the project's bucket
create_job → create the job (python, spark, bash or lowcode — nothing else)
run_job → start a first execution
get_run · get_run_logs → follow it, and read the logs by cursor
schedule_job → set the cron

Each 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.

  • 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 a DELETE operation.
  • A known job type. create_job only accepts python, spark, bash and lowcode, 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.