> ## Documentation Index
> Fetch the complete documentation index at: https://docs.danubeai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect a System

> Give your agents a governed way into an internal API, a database or an MCP server from one input, then shape what they may do and keep the calls worth keeping

Connecting a system takes one input: the base URL of an API, a reference to a database connection, or the URL of an MCP server. Danube discovers what it can and hands your agents a small governed surface at once. You shape that surface with a policy, and turn the calls agents actually make into named tools later. Nobody enumerates endpoints or writes query templates to get started.

The pieces are documented on [Organizations](/organizations/overview), [Policies](/organizations/policies), [Audit Log](/organizations/audit-log) and [Data Plane](/organizations/data-plane); a database has its own page, [Connect a Warehouse](/organizations/connect-a-warehouse).

## Prerequisites

* An organization you **own or administer**. Members use connected systems; only owners and admins connect them.
* For a system that is only reachable inside your network, an owner on the **Team or Enterprise plan** and a running [data-plane agent](/organizations/data-plane). A database always runs through the agent on the hosted service.
* The one input: a base URL (and the key or token the API expects), an `env://` or `vault://` reference to a connection string, or an MCP server URL.

## 1. Connect

| Kind | You give | Your agents can do right away |
| - | - | - |
| `api` | The base URL; how it authenticates; an OpenAPI URL if the document is not at a well-known path | Every operation the OpenAPI document declares, when one is found, plus one generic **Request** tool: any `GET` or `HEAD` on any path, under the policy |
| `connector` | A connector (PostgreSQL, MySQL, ClickHouse, Redis, Kafka, Kubernetes, CloudWatch, DynamoDB, Lambda, SQS, S3, Redshift, Grafana, Prometheus, New Relic, Firebase, Salesforce), a name and its connection | That connector's full tool set, named after your system ("Orders DB - Run Query"), under the system's rules. See [Databases and infrastructure](#databases-and-infrastructure) |
| `database` | A warehouse engine without a native connector (Snowflake, BigQuery, Databricks, Trino) and a connection reference | **List tables**, **Describe table** and **Query**: one read-only statement at a time, on the tables the policy allows, capped and masked |
| `mcp` | The server URL and its auth | The server's own tools, discovered in the background |

### Reachability

| The system is | Connect it as | What runs the call |
| - | - | - |
| Reachable from the internet (a hosted service, a public endpoint behind an API key) | `internal` | Danube, with the credential injected at call time |
| Reachable only inside your network (a private IP, an internal DNS name, a database) | `local_only` | Your [data-plane agent](/organizations/data-plane), which receives the definition and the parameters plus credential *references*, resolves the secret locally and makes the call |

Both are visible to every member of the organization and nobody else. An `internal` API is probed for an OpenAPI document at the usual paths (`/openapi.json`, `/swagger.json`, `/v3/api-docs`, and so on); a `local_only` one is not, since Danube never reaches it, but an `openapi_url` you pass is still read. A database defaults to `local_only`.

### From the dashboard

<Steps>
  <Step title="Open Connect a system">
    Go to [Dashboard > Connect a system](https://danubeai.com/dashboard/connect) and pick the organization.
  </Step>

  <Step title="Choose the kind and paste the one input">
    An API's base URL with its authentication, a database's engine and connection reference, or an MCP server URL. Choose *Internal* or *Runs inside your network*.
  </Step>

  <Step title="Preview">
    The preview is a dry run: discovery runs, the tools it would create are listed, and nothing is written. For a database you see the tables and columns it found.
  </Step>

  <Step title="Connect">
    The service appears on the Tools page under your organization's tab, with its policy card and, for a database, the discovered tables ready for the allowlist.
  </Step>
</Steps>

### Over the API

```bash theme={null}
curl -X POST "https://api.danubeai.com/v1/organizations/ORG_ID/connect" \
  -H "danube-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "api",
    "name": "Inventory",
    "description": "Stock levels and reorders",
    "visibility": "internal",
    "api": {
      "base_url": "https://inventory.example.com",
      "auth": {"type": "api_key", "location": "header", "name": "X-API-Key"},
      "openapi_url": "https://inventory.example.com/docs/openapi.json"
    },
    "dry_run": true
  }'
```

`kind` is `api` (with `api.base_url`, optional `api.auth` in the [connector auth](/connectors#authentication-methods) shape, optional `api.openapi_url`), `database` (with `database.db_type` and `database.connection_ref`, optionally `database.schema` to limit discovery and `database.max_rows`) or `mcp` (with `mcp.server_url`, `mcp.transport`, `mcp.auth_type`, `mcp.auth_header_name`). `visibility` is `internal` by default, `local_only` for a database. Run it once with `"dry_run": true`, then without.

The response is the same either way, with `service.id` null on a dry run:

```json theme={null}
{
  "service": {"id": "…", "name": "Inventory", "visibility": "internal", "metadata": {"access_policy": {…}, "discovery": {…}}},
  "tools": [
    {"id": "…", "name": "Inventory - Request", "generic": "request", "method": "REQUEST", "path": "/"},
    {"id": "…", "name": "inventory.list_items", "generic": null, "method": "GET", "path": "/items"}
  ],
  "discovered": {"kind": "api", "openapi": true, "openapi_url": "https://inventory.example.com/docs/openapi.json", "endpoint_count": 12, "tables": [], "discovered_at": "…"},
  "policy": {"http": {…}, "sql": {…}},
  "sync": "done"
}
```

`discovered` is stored on the service as `metadata.discovery`. For a database it carries `tables`, each with `schema`, `name` and `columns` (capped at 500 tables and 200 columns per table), which is what the allowlist picker on the service page reads. When discovery could not run, `discovered.error` says why (`no_dataplane_agent` is the usual one: connect the agent, then use **Discover again** on the service page, or `POST /v1/organizations/{org_id}/connect/{service_id}/discover`). `sync` is `running` for an MCP server whose tools are being listed in the background.

Every connect is recorded as `service.create` with the kind, the visibility and what discovery found.

### Databases and infrastructure

In the dashboard, **Connect a system > Database or infrastructure** lists every connector. Pick one, give the system a name your team will recognize ("Orders DB", "Prod cluster", "Payments events") and fill in the connection form, the same fields as the [connector's page](/connectors/production-database) describes. Then:

1. **Test connection** runs the connector's Test Connection with what you typed and lists the system's resources: tables, namespaces, topics, log groups, buckets, and so on. A failed test says whether authentication, the network or TLS failed, and nothing is created.
2. **Connect** creates the system: its own copy of the connector's tools, named after it and described with the resources discovery found, so agents (and search) can tell your databases apart.
3. The connection is stored once as the organization's shared credential, with the rule **Shared**: every member's agent uses it, and nobody's personal credential for the same service takes its place. Store secrets as `env://` or `vault://` references to run the system on your [data-plane agent](/organizations/data-plane) instead of connecting from Danube.

Over the API the kind is `connector`:

```json theme={null}
{
  "kind": "connector",
  "name": "Orders DB",
  "connector": {
    "type": "postgres",
    "connection": {"host": "db.internal", "database": "app", "username": "danube_readonly",
                   "password": "env://ORDERS_DB_PASSWORD", "mode": "read_only"}
  },
  "dry_run": true
}
```

`GET /v1/infra-connectors/catalog` lists the connector types with their connection forms. A system's rules live in the policy's `connector` section (see Govern below). Every connector also has a catalogue entry ("PostgreSQL", "Kubernetes") that one person can connect for themselves; its page links to setting it up for the organization instead.

## 2. Govern

A connected service carries an **access policy** (`metadata.access_policy`) that its generic tools obey. It starts strict: reads on every path, and no database table until you list some. `GET /v1/organizations/{org_id}/services/{service_id}/policy` reads it, `PUT` replaces it, and the policy card on the service page edits it.

A **database or infrastructure system** is governed by the `connector` section instead, and it starts open: everything the connection's identity can reach, read-only unless the connection's mode is read and write.

| Key | Meaning |
| - | - |
| `allowed` | Resources agents may use: table names (`public.orders`), namespaces, topics, log groups, key patterns, with `*` and `?` wildcards. Empty allows everything |
| `denied` | Always refused, even when `allowed` matches |
| `masked_columns` | Result columns whose values come back as `[MASKED]` |
| `max_rows` | The most rows or items one call returns (up to 5,000) |
| `writes` | `off` refuses every write tool, `consent` (the default) needs a confirmation on every write, `allowed` runs writes without one. Writes also need the connection's mode set to read and write |
| `defaults` | Parameter values filled into calls that leave them out, such as a Postgres `schema`, a Kubernetes `namespace` or a CloudWatch `log_group`. Discovery suggests some when you connect |

The rules apply to every call, whether it runs from Danube or on your data-plane agent: a parameter naming a resource is checked, so are the tables a SQL statement reads, list results only show allowed resources (`hidden_by_policy` counts the rest), and a call that asks for every Kubernetes namespace is refused while the namespaces are narrowed.

| Field | What it decides |
| - | - |
| `http.allowed_methods` | The methods the Request tool may send; `GET` and `HEAD` by default. A write method listed here still needs `writes` to allow it |
| `http.allowed_path_prefixes` | Paths the Request tool may reach, by prefix; `/` (everything) by default |
| `http.denied_path_prefixes` | Prefixes refused whatever the allow list says (`/admin`, `/internal`) |
| `http.writes` | `off` (POST, PUT, PATCH and DELETE are refused), `consent` (a write runs only after a person confirms it, through the same handshake as any other destructive call) or `allowed` |
| `sql.allowed_tables` | The tables **Query** may read and **List tables** shows; empty means none. `schema.table` or `table`, case-insensitive |
| `sql.denied_tables` | Tables refused whatever the allow list says |
| `sql.masked_columns` | Columns returned as `[REDACTED:<column>]` (`table.column` or `column`), and marked as masked by **Describe table** |
| `sql.max_rows` | The row cap per call, 1,000 at most |

A refusal is a normal failed call, not an outage: `error_type: permission_denied`, `fault: caller`, with a message that names the method, path or table. Agents are told to read it that way (the [Agent Skill](/skill) says a refused path or table is policy, not a bug) and to call **List tables** and **Describe table** before **Query**. Named tools you promote are not subject to the policy; they do exactly what their definition says.

Two more layers apply to every tool of the service, generic or named:

* **Grants** ([Policies](/organizations/policies)) decide which members, teams and roles may call the service or a tool at all. With no grants, every member can. The first grant flips the organization to deny-by-default for its own tools; a member who is not granted a tool does not see it, and a call answers `403 Your organization does not have access to this tool`.
* **Credentials.** An internal service usually authenticates with one key the company holds. Store it once for the organization, and every member who has no credential of their own for the service uses it:

```bash theme={null}
curl -X PUT "https://api.danubeai.com/v1/organizations/ORG_ID/credentials/SERVICE_ID" \
  -H "danube-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"credentials": {"type": "api_key", "value": "sk-live-...", "header_name": "X-API-Key"}}'
```

For a `local_only` service the value must not be a value at all: store a **reference** the agent resolves inside your network, `env://VAR_NAME` or `vault://<mount>/<path>#<field>`. A credential held only as a value in Danube is never shipped to the agent; the call fails with `credential_reference_unresolved`. A database's only secret is the connection string, which lives behind the `connection_ref` you gave at connect time.

## 3. Curate

Generic tools get agents working on day one. The calls worth keeping become named tools, with a name an agent can search for and a fixed shape a reviewer can read.

From the [Audit Log](https://danubeai.com/dashboard/organization/audit), open a `tool.execute` row on a Request or Query tool and choose **Promote to tool**. Over the API:

```bash theme={null}
curl -X POST "https://api.danubeai.com/v1/organizations/ORG_ID/services/SERVICE_ID/promote" \
  -H "danube-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Low stock items",
    "description": "Items whose stock is under the reorder point",
    "execution_id": "EXECUTION_ID"
  }'
```

With `execution_id` the call is read from the execution row: an HTTP call becomes a tool with that method and path, the path's `{placeholders}` as path parameters and the query keys as optional parameters with the values used as defaults; a SQL call becomes a query template with its `:name` placeholders as parameters. Without one, pass the call yourself: `"request": {"method": "GET", "path": "/items/{sku}", "query": {"expand": "supplier"}}` or `"sql": {"sql": "SELECT sku, stock FROM items WHERE stock < :threshold", "parameters": [{"name": "threshold", "type": "integer"}]}`. In [data residency mode](/organizations/data-plane) the execution row holds only a digest of the parameters, so the explicit fields are required. The named tool is returned, and the write is recorded as `tool.create` with `promoted_from`.

The advanced paths are still there for a system that needs them: `POST /v1/organizations/{org_id}/services` registers a service from an OpenAPI document you have on hand, a hand-written [connector spec](/connectors) or a set of query templates, and the [Connector Builder](https://danubeai.com/dashboard/tools/connector) edits those specs in the dashboard. Both produce named tools directly, without a generic surface or a policy.

## 4. Verify

1. **Test run.** Open the service on the Tools page and run **List tables** or the Request tool with a `GET`, or over the API `POST /v1/tools/call/{tool_id}` from a member's key. A `local_only` tool needs an agent online: the Data Plane page shows each agent's **Online** badge.
2. **Audit log.** Filter [Audit Log](https://danubeai.com/dashboard/organization/audit) by the service. You should see the `service.create` row from the connect and one `tool.execute` row per call, with the key that made it and a hash of the parameters. A `tool.denied` row means a grant is missing; a `permission_denied` failure means the policy refused the method, path or table.
3. **A second member.** Have someone with the `member` role search for the service. Seeing it confirms visibility and the grants.

## 5. Operate

| Concern | Where | What it does |
| - | - | - |
| Calls per minute, daily spend | Organization > Settings > Limits, or `rate_limit_per_minute` / `daily_spend_cap_cents` on `PATCH /v1/organizations/{org_id}/settings` | Caps every call made in the organization's context, on top of per-key caps. Over the cap: `429` with `Retry-After`, or `403`, both `usage_limit_reached`, both audited |
| Execution history | Organization > Settings, or `execution_retention_days` | Deletes the organization's execution rows older than the setting, nightly |
| Audit retention | Organization > Settings, or `audit_retention_days` | Deletes audit events older than the setting (30 days minimum), nightly |
| Data residency | Organization > Settings, or `dataplane.store_payloads: false` | Data-plane parameters and results are never written to Danube; only a size and a SHA-256 are kept. Promotion then needs the explicit `request` or `sql` |
| Hosted AI | Organization > Settings, or `llm_internal_tools: false` | Keeps the organization's internal tool definitions and results out of hosted workflows and the workflow generator |
| Organization keys | API Keys, the organization's tab | Keys that belong to the organization rather than a person; admins can revoke any of them, and a departing member's are revoked on removal |
| Changes to the service | The service page (owner, or an owner / admin of the organization) | Edit the policy, discover again, refresh tools, deprecate or delete; every write is audited |

## Troubleshooting

Connect errors, from `POST /v1/organizations/{org_id}/connect` and the dashboard's preview:

| Status | Message | Fix |
| - | - | - |
| `400` | `api is required for kind api` (and the same for `database`, `mcp`) | Send the block the chosen `kind` needs |
| `400` | `base_url must be an http or https URL with a host` | Give the scheme and host |
| `400` | `base_url is not reachable under this deployment's policy: ...` | The hosted service refuses private addresses for an `internal` system; connect it as `local_only` and run the data-plane agent |
| `400` | `A database runs only through the organization's data-plane agent on the hosted service; connect it with visibility local_only` | A database on the hosted service is always `local_only` |
| `400` | `connection_ref ...` (a value where a reference belongs) | Pass `env://NAME` or `vault://<mount>/<path>#<field>`, never the connection string |
| `400` | `unsupported db_type '...'` | One of `postgres`, `mysql`, `redshift`, `snowflake`, `bigquery`, `databricks`, `clickhouse`, `trino` |
| `400` | `Cannot reach MCP server at <url>: ...` / `Invalid MCP server URL: ...` | An `internal` MCP server must answer a `HEAD` from Danube; one Danube cannot reach belongs under `local_only` |
| `403` | `Insufficient permissions for this action` | You are a member but not an owner or admin |
| `404` | `Organization not found` | You are not a member of that organization, or the id is wrong |
| `409` | `A service with this name already exists` | A service you cannot manage already carries the name; choose another. A same-named service you do manage is updated, and its policy is kept |
| `413` | `OpenAPI document is N bytes; the limit is 5,000,000 bytes` | Point `openapi_url` at a smaller document, or leave it out and rely on the Request tool |

Promote errors, from `POST /v1/organizations/{org_id}/services/{service_id}/promote`:

| Status | Message | Fix |
| - | - | - |
| `400` | `The execution's parameters were not stored (residency mode); pass request or sql explicitly` | Add `request` or `sql` |
| `400` | `Only a call on a generic Request or Query tool can be promoted` | The execution was on a named tool already |
| `400` | `sql: ...` | The statement is not a single read-only `SELECT` |
| `404` | `Execution not found` | The id is wrong, or the execution belongs to another service |

Execution errors, in the `error_type` of a failed call:

| `error_type` | Meaning | Fix |
| - | - | - |
| `permission_denied` | The access policy refused the method, the path or the table (`fault: caller`) | Widen the policy on the service page, or promote the call as a named tool with the shape you want |
| `no_dataplane_agent` | No agent of the organization has polled in the last two minutes | Start or enroll an agent; the Data Plane page shows who is online |
| `destination_blocked` | The agent refused the host because it is outside `DANUBE_ALLOWED_DESTINATIONS` | Add the host to the agent's allowlist |
| `upstream_tool_missing` | The MCP server no longer offers the tool the catalog row points at | **Refresh tools** on the service page |
| `credential_reference_unresolved` | The `env://` or `vault://` reference could not be resolved on the agent's host | Set the variable on the agent, or check `VAULT_ADDR` / `VAULT_TOKEN` and the `#<field>` suffix |
| `auth_required` | No credential, personal or shared, for the service | Store one (step 2) |
| `confirmation_required` | The policy has `writes: consent` and the call was a write | Confirm the call through the consent handshake; an agent shows the user the call and repeats it with the token |
| `usage_limit_reached` | A per-key or organization cap | Read `Retry-After` for the per-minute cap; raise or wait out the daily cap |

And the two refusals that come back as an HTTP status: `403 Your organization does not have access to this tool` (no grant, or an explicit deny; ask an admin for a grant) and `404 Tool not found` for someone outside the organization, which is deliberate so an internal tool's existence is not revealed.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.