# Agent-2-Agent
Integrate Gatana as MCP Gateway in your Agent-2-Agent architecture
## The Challenge
In Agent-2-Agent (A2A) architectures, a centralized service orchestrates AI agents that need access to various tools and APIs. The fundamental challenge is **identity and credential propagation**:
* How does a new employee immediately get access to the right tools?
* How do you inject team-specific API credentials without per-user configuration?
* How does the A2A service call downstream tools on behalf of the user?
Gatana solves this by combining **OIDC token trust**, **claim-based profile mapping**, and **credential overrides**—enabling zero-touch onboarding for new users.
## How It Works
```
┌──────────────┐ ┌─────────────────┐
│ End User │──── Authenticates ──────────►│ Corporate IdP │
│ (new hire) │ │ (Okta, Entra) │
└──────────────┘ └────────┬────────┘
│
Access Token with claims:
{ team: "platform", ... }
│
▼
┌──────────────┐ Token forwarded ┌─────────────────┐
│ A2A Service │◄────────────────────────────│ A2A Agent │
│ (Orchestrator) │ (e.g. Slack) │
└──────┬───────┘ └─────────────────┘
│
│ Same token passed to Gatana
▼
┌─────────────────────────────────────────────────────────────┐
│ Gatana Gateway │
│ │
│ 1. Validates token via OIDC Token Trust │
│ 2. Extracts claims (team: "platform") │
│ 3. Applies matching profile (platform-team) │
│ 4. Injects profile credentials (Logz.io token, etc.) │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Logz.io MCP │ │ GitHub MCP │ │ Jira MCP │
│ (team token) │ │ (user OAuth) │ │ (shared creds) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
See [Zero Touch Onboarding](/zero-touch-onboarding) for instructions on how to setup zero-trust onboarding.
**New hire onboarding flow:**
1. Employee joins and is added to `platform-team` group in IdP
2. Employee opens their AI assistant, which calls the company A2A service
3. A2A service forwards their access token to Gatana
4. Gatana validates token, sees `team: platform` claim, applies `platform-team` profile
5. Employee immediately has access to Logz.io, AWS, and GitHub tools with correct credentials
## Related Documentation
* [Zero Touch Onboarding](/zero-touch-onboarding) - Setup Zero Touch Onboarding
* [Identity Federation](/federation) — Configure OIDC/SAML and token trust
* [Profiles](/profiles) — Credential overrides, restrictive mode, and claim mappings
* [Credentials](/credentials) — How credential scoping works
# AI Assistant
Ask Gatana how the product works, and let it act in your organization
## Introduction
The AI assistant is a chat panel in the Gatana dashboard. Open it with the **Ask Gatana** button in
the header, or with ⌘J (Ctrl+J on Windows and Linux). Some pages
also show an **Ask AI** button, which opens the panel with the question already written.
The panel opens over the page. The pin button at the top docks it beside the page instead: the
dashboard makes room for it, you keep navigating with the conversation on screen, and you can drag
its left edge to resize it. The pin and the width are remembered between visits.
The assistant does two kinds of work:
* **It answers questions about Gatana.** It searches the documentation and reads the page you are
on, so it can answer "why is this server greyed out" as well as "how do I share one credential
with a team".
* **It acts in your organization.** It can list, create and change the same things you can in the
dashboard — servers, profiles, teams, credentials and the rest — through the
[Gatana Public API](/public-api-introduction).
Three rules hold for everything it does:
1. It acts with **your** permissions. It cannot reach anything you could not reach yourself.
2. Anything that would change your organization is **shown to you for approval** before it runs.
3. Every action is **audit-logged**, exactly like an action you take by hand.
## The assistant's own tools
Four tools are the assistant's own. They are always there, and no profile grants or withholds them:
| Tool | What it does |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `search_docs` | Searches this documentation. It is how the assistant answers "how does X work" with the current answer instead of a remembered one. |
| `read_current_page` | Asks your browser what the page you are on is showing. Your browser answers it, not the server. |
| `ask_user_question` | Puts a question to you, with the answers as buttons, and waits. The assistant uses it when it needs a decision that is yours — most often to offer to do a thing it has just described. Pick an answer, type one of your own, or ignore the card and keep writing. |
| `search_tools` and `execute_code` | [Code Mode](/code-mode), when the profile grants it. |
| \* | Any other tools you [promote in its profile](#promoted-tools). |
## Profile
What the assistant may use is an ordinary [profile](/profiles). Your organization owns it, and it is
listed, edited and assigned like any other profile. The servers on that profile are the servers the
assistant can reach; its [tool restrictions](/profiles#tool-restrictions) apply as they would to any
client.
The profile is created the first time somebody uses the assistant. It is named **AI Assistant**, and
it starts with the two built-in servers the assistant needs to work on your organization:
| Server | What it gives the assistant |
| ------------ | -------------------------------------------------------------------------------------- |
| `codemode` | [Code Mode](/code-mode): `search_tools` to find tools, and `execute_code` to call them |
| `gatana-api` | The Gatana Public API, which is how it acts on your organization |
To change what the assistant can reach, open the profile from **Profiles** in the side menu, or from
the settings button at the top of the assistant panel. Add a server to let the assistant use it;
remove one to take it away. If you delete the profile, the next conversation creates a fresh
default.
A profile gives **member-level permission** on every server assigned to it, to every session that
applies the profile. The assistant's profile is an ordinary profile, so a server you add to it
becomes reachable by anyone the profile is applied to — through direct assignment, a team, a claim
mapping, the `p=` parameter, or the **Open to All Users** flag. It is created unassigned and
closed. Leave it that way, and check it before you add a sensitive server to it.
An assistant turn is a different case. It runs on the permissions of the person using the panel,
which the profile does not raise: the profile says which servers the assistant may use and which of
their tools, and the user's own permissions decide what may actually run. So a server on the profile
that you cannot reach yourself stays out of reach in the panel. The warning above is about everyone
else the profile might be applied to, not about the panel.
### Code Mode and the long tail
Most of what the assistant can do goes through [Code Mode](/code-mode). Rather than putting hundreds
of tools in front of the model, Gatana gives it `search_tools` to find the right tool and
`execute_code` to call it. That is why the `codemode` server is on the profile by default.
Code Mode alone gives the assistant nothing to act on. It needs at least one other server on the
profile — `gatanaApi`, or one of your own — before `search_tools` has anything to find.
## Promoted tools
A [promoted tool](/profiles#promoted-tools) is offered to the assistant directly, by name, instead of
being reached through `execute_code`. This is useful to get ahead of discovery. The assistant will always be aware of the tool. For example, see [the web](#web-access) below.
To promote a tool for the assistant:
1. Open the **AI Assistant** profile
2. In the servers table, click **Edit** on the server row
3. Turn on **Promote** for the tool
4. Click **Save Changes**
What promotion changes:
| | Through Code Mode | Promoted |
| ------------------------ | -------------------------------------------------------- | ---------------------------------- |
| How the model reaches it | Finds it with `search_tools`, calls it in `execute_code` | Calls it by name, directly |
| Context cost | Only when it searches | On every turn, whether used or not |
| Best for | The long tail: hundreds of tools, used rarely | A handful of tools, used often |
Because a promoted tool spends context on every turn, at most **30** of them are put in front of the
model. Anything above that stays reachable through Code Mode.
A tool that says it only reads runs straight away. A tool that can change something asks you first,
on every call — see [Approvals](#approvals).
A promoted tool is offered under its [exposed name](/profiles#renaming-tools). The assistant
reduces a name that a model provider would refuse to letters, digits, hyphens and underscores. If
two tools end up with the same name, the second one is numbered.
## Letting the assistant use the web
To enable searching the web or fetching a web page/URL: simply add these tools to the [assistants profile](#the-assistant-profile)
| Job | Promoted name | What it does |
| --------------- | ------------- | ------------------------------------------------- |
| Search the web | `search-web` | A search API, such as Brave or Tavily |
| Read a web page | `fetch-web` | A fetcher that returns the content of one address |
Any tool of any server you have connected can do either job. The name is what tells the assistant
which tool does what, so promote the tool under exactly `search-web` or `fetch-web`.
The quickest way is the settings button at the top of the assistant panel: pick a tool for each job
and Gatana writes the promotion for you, adding the server to the profile if it is not there yet.
You can do the same by hand on the profile page.
Who can do it: an organization owner, or a [maintainer](/profiles#profile-maintainers) of the
assistant's profile. Everyone else sees the panel without the settings button.
## Approvals
Reading runs immediately. Anything that creates, changes or deletes is put to you first, in the
conversation, as a card that shows exactly what will run:
* For **code**, the program the assistant wrote, and the tools in it that can change something.
* For a **promoted tool**, the tool and the arguments it would be called with.
You then choose:
| Choice | What happens |
| ----------------------- | ----------------------------------------------------- |
| **Run it** | It runs once, with these exact arguments. |
| **Always allow `tool`** | It runs, and you are not asked about that tool again. |
| **Cancel** | Nothing runs. The assistant is told you said no. |
An approval is bound to the exact code or arguments you saw. If the assistant changes either one, it
has to ask again. Approvals expire, so a card you come back to hours later asks the assistant to
request approval anew.
**Always allow** is yours alone — it is not granted for your colleagues, and it removes the question
and nothing else. Whether you may use the tool is still decided at every call by your permissions,
the profile and the [tool firewall](/tool-firewall). Take a standing approval back under **Tools you
always allow** in the assistant's settings.
## What the assistant sees of your screen
Every question carries the address of the page you are on and the identifiers on it, so that "this
server" and "the second one" mean something. When the answer needs more — which rows are listed,
what is filtered, what you have typed but not saved — the assistant asks your browser for an outline
of the page. Secrets are removed before it is sent.
The assistant treats everything that comes back as data, never as instructions. It confirms anything
it is about to act on with the API first, because a page can be stale.
## Enabling the assistant
The assistant is **off until an organization owner turns it on**. The first time the panel is
opened it offers the opt-in: an owner can turn the assistant on right there, and everyone else is
told that an owner has to. An owner can also turn it on — and off again at any time — under
**Settings → Features → AI assistant**, which is also where the model is chosen.
On Gatana's model, one more thing is asked of an owner: that Gatana's model provider may process
your conversations. Conversations — including what the assistant reads from your organization while
answering — are sent to OpenRouter under a zero-data-retention policy: prompts and answers are not
stored once the reply is served. OpenRouter is listed as a sub-processor in the
[Data Processing Agreement](https://www.gatana.ai/dpa), and agreeing is your organization's
instruction to process conversations this way. It is recorded with the time and the owner who gave
it, and can be withdrawn under **Settings**.
That question is asked only when the conversations would go there. An organization
[bringing its own model](#byok) is not asked it, because nothing reaches that provider. It is asked
if such an organization later moves to Gatana's model — the instruction covers the processing, not
the switch, so it is not read into a decision made about something else.
Each user keeps their own conversation history, reachable from the **History** button in the panel.
## Sharing conversations to improve the assistant
On Gatana Cloud, Gatana can keep a reduced copy of assistant conversations to find and fix the
assistant's own gaps. Whether it does depends on your plan:
* **Free plan**: sharing is a term of the plan. It is always on and cannot be turned off.
* **Paid plan**: sharing is off until an organization owner turns it on. The choice is offered when
the assistant is turned on, and an owner can change it at any time in the assistant's settings or
under **Settings → Features → AI assistant**. The agreement is recorded with the time and the
owner who gave it.
* **On-premise**: nothing is collected, and the choice is not shown.
An organization that moves from the free plan to a paid one stops sharing at that moment, and stays
that way until an owner chooses otherwise.
The copy keeps the conversation's text and structure, with the values taken out:
* Tool arguments are reduced to their shape: `{ slug: 'github' }` becomes `{ slug: '' }`.
* Tool results are dropped. Error messages are kept.
* Any tool call whose name suggests a credential or a secret is dropped entirely, and secrets are
scrubbed from everything that remains.
A conversation is copied only after it has been quiet for a few hours, so none is taken mid-way.
The copy is stored encrypted and is deleted after 90 days.
Turning sharing off deletes every copy Gatana still holds, not only future ones.
## Bringing your own model
An organization can point the assistant at a model account of its own instead. Conversations then go
to that endpoint, on your inference budget, and Gatana sends nothing to OpenRouter — the
sub-processor entry above stops applying to the assistant. Choose **BYOK** under
**Settings → Features → AI assistant**, where the daily token limit also appears, since the spend
becomes yours to cap.
Three kinds of endpoint are supported. They differ in how a request is authorized rather than in
what is asked of the model:
| Provider | What to enter |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OpenAI-compatible endpoint | Base URL, model, API key. Anything that speaks the OpenAI chat completions protocol: vLLM, DeepSeek, Together, OpenRouter under your own key, or a gateway of your own |
| AWS Bedrock | Region, model or inference profile ID, and either a Bedrock API key or an IAM access key pair (with a session token, if the pair is temporary) |
| Azure OpenAI Service | Resource name, deployment name, API key, and optionally an API version to pin |
Credentials are encrypted with your organization's key and are never sent back to the dashboard. To
change a model, a region or a deployment later, leave the credential boxes empty and the stored
credential is kept; type a new one to rotate it.
If the endpoint cannot be used — no credential, an incomplete one — the assistant stops and says so
rather than falling back to Gatana's provider, so nothing leaves your account unnoticed. Switching
back to **Gatana-provided** leaves the credentials stored, so switching to your own account again
does not mean entering them a second time.
# Alerts & Notifications
Receive email or webhook notifications when audit log events match your rules
Gatana can send an email or call a webhook when events in your [audit log](/audit-logs) match a rule. For example, you can get an alert when a protection detects a secret or a tool fails repeatedly.
Open **Alerts** in the left sidebar. Only organization owners can manage alerts. All plans support rules and a preview of matching events. Notification channels require a paid plan or a self-hosted installation.
## What you can monitor
| Event | Examples |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| [Protection checks](/tool-firewall#protections) | A check detects sensitive data or blocks a call |
| Denied tool calls | The firewall or another access check denies a call |
| Tool failures | A tool returns an error or cannot be reached |
| Configuration changes | Someone changes a server, credentials, organization settings, SIEM streaming, an alert rule, or a notification channel |
| Sign-ins | Someone signs in successfully |
Built-in templates cover these cases. You can also create custom rules with the [Audit Logs filters](/audit-logs#filtering), except date filters. Add conditions to limit a rule to a specific server, tool, user, or event.
## Create a rule
1. On the **Channels** tab, select **New channel**. Add email recipients or a webhook URL, then use **Send test** to check delivery.
2. On the **Rules** tab, select **New rule**. Give it a name. This name becomes the notification title.
3. Select **Use a template** or add your own conditions. An event must match every condition. Use separate rules for alternative conditions.
4. Set the number of events needed for an alert. For example, notify after **10 events within 5 minutes**. Set the minimum wait between notifications to control how often the rule sends.
5. Select **All channels** or **Selected channels**, then save the rule. **All channels** also includes channels that you add later.
**Matching events** shows counts for the last 7, 30, and 90 days while you edit. Each count links to the matching audit events.
By default, one matching event triggers an alert, with at least 60 minutes between notifications. The next notification includes events that match during this wait.
Gatana checks rules once a minute. Notifications usually arrive within one to two minutes. Rules monitor new events. Creating or enabling a rule, or changing its conditions, skips existing events.
Tool-call rules need MCP audit logging. The default level, `terse`, is sufficient. At `off`, tool-call rules cannot match. See [Logging Detail](/audit-logs#logging-detail).
## Channels
| Type | Support |
| ----------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Email** | Up to 10 addresses per channel, including distribution lists. Recipients do not need a Gatana account to receive the email |
| **Webhook** | A signed JSON `POST` to an HTTPS URL, with an optional authentication header |
Notifications contain the rule name, event count, time range, and a link to the matching audit events. They exclude the event details, tool arguments, and responses. The link requires a Gatana sign-in and permission to view the events.
You can test or disable each channel. Self-hosted email requires SMTP or Amazon SES.
### Webhook requirements
* Use a public HTTPS address in Gatana Cloud. Self-hosted installations can also use private network addresses.
* Use the optional extra header for authentication, such as `Authorization`. Gatana encrypts the stored header value.
* Verify `X-Gatana-Signature` with the channel's signing secret. The [SIEM signature verification examples](/siem-streaming#verifying-signatures) also work for notifications.
* Return a `2xx` response within 10 seconds. Gatana does not follow redirects. Retries can cause duplicate messages; use the notification's `id` to detect duplicates.
## History and limits
The **History** tab keeps notifications for 90 days and shows the delivery result for each channel. Gatana makes up to five delivery attempts per notification and channel. After 24 hours of channel failures, organization owners receive an email.
| Limit | Maximum |
| ------------------------------------------------ | ------- |
| Rules per organization | 50 |
| Conditions per rule | 20 |
| Channels per organization | 20 |
| Alert notifications per organization per UTC day | 500 |
| Channel tests per organization per hour | 20 |
An alert counts once toward the daily limit, even if it goes to several channels. Tests do not count toward this limit.
At the daily limit, alerts pause until midnight UTC and owners receive an email. Events during the pause remain in the audit log but do not trigger alerts later.
Changes to rules and channels appear in the audit log and in [SIEM streaming](/siem-streaming).
# Artifacts
Let agents publish pages and documents at a stable link, and share them with people, teams, or the world
## Introduction
An artifact is a document an agent publishes through Gatana: a report, a dashboard, a small tool, or any self-contained page. Each artifact gets a stable link on your organization's domain:
```
https://{organization}.gatana.ai/artifacts/{id}
```
The link never changes: not when the content is updated, not when the artifact is renamed, and not when its access changes. Send the link once and it keeps pointing at the newest version.
Agents create artifacts with the built-in **Artifacts** server. Its tools are `create_artifact`, `update_artifact`, `share_artifact`, `unshare_artifact`, and `list_artifacts`. The same operations exist as `/artifacts` routes in the [Public API](/public-api-introduction).
All artifacts you can open are listed under **Artifacts** in the dashboard sidebar.
## Who Can Open An Artifact
A new artifact is **private to its creator**. Access is widened in two independent ways, and the Share button in the viewer controls both:
**General access** sets the audience for the link:
| Access | Who can open the link |
| ---------------- | ----------------------------------------------------------- |
| **Private** | Only the creator and the people and teams it is shared with |
| **Organization** | Every signed-in member of your organization |
| **Public** | Anyone with the link, no sign-in needed |
**Shares** give read access to chosen people or teams, whatever the general access is. A team share covers every current member of the team. Agents share by email address and team name with `share_artifact`; people use the Share button in the viewer. Shares on an organization-wide or public artifact change nothing at that moment, but they are kept, so setting the artifact back to private restores exactly the earlier audience.
Sharing gives read access only. Renaming, sharing, changing access, and deleting stay with the creator and organization owners.
## Content: Markdown or HTML
An artifact holds either Markdown or a complete HTML document, chosen per version:
* **Markdown** is stored as written and rendered to a styled page when opened. GitHub-flavored syntax is supported (tables, task lists, fenced code). This is the right choice for documents, reports and notes. The viewer offers **Copy Markdown** to get the source back.
* **HTML** is served as published. The page must be self-contained: inline the CSS and JavaScript and embed assets as data URIs, because the page renders in a sandboxed frame with no access to Gatana.
A Markdown version can be at most 10 MB, an HTML version at most 50 MB.
### Themes
Each artifact has a theme. The **Gatana** theme gives plain semantic HTML and rendered Markdown a finished look (typography, tables, code blocks, links) without the author writing any CSS. Choose **None** to serve the document exactly as published. The theme is changed in the viewer's title menu, applies to every version at once, and never creates a new version. The author's own styles always win over the theme.
#### Organization themes
Owners can add themes with the look of their organization. Open **Artifacts**, then **Themes**. An organization theme is the Gatana theme with your own:
* **Colors**: background, text, and accent. The other tones (body text, lines, soft backgrounds, button hover) are mixed from these three, so a dark background works too.
* **Fonts**: for headings, body text, and code. Gatana serves the fonts itself, so a page never loads a font from a third party.
* **Logo**: a PNG, JPEG, WebP or SVG image of at most 256 KB. It shows at the top of every page on the theme.
* **Extra CSS**: added after the theme. The page's own CSS still wins over it.
The editor shows a live preview while you change the theme. The preview embeds the selected fonts and logo and blocks network requests, including external images, fonts and stylesheets in extra CSS. This restriction applies only to the theme preview. All Gatana classes and variables work under an organization theme, so agents write a page the same way for every theme.
Every member can pick an organization theme for their artifacts. Agents name it by its name or its id: the `list_artifact_themes` tool of the **Artifacts** server lists the themes. A change to a theme reaches every artifact on it the next time the artifact is opened. When an owner deletes a theme, its artifacts go back to the Gatana theme.
#### Default theme
The **Themes** page also sets the default theme: the theme a new artifact gets when its creator does not name one. It starts as Gatana. A change to the default does not change existing artifacts.
## Versions
Every content update creates a new version behind the same link, and the link always shows the newest one. Older versions stay available under **Version history** in the viewer's title menu. Renaming an artifact or changing its theme or access does not create a version.
Deleting an artifact deletes all versions, and the link stops working.
## The Viewer
Opening an artifact link shows the page with a small header: who created it, when it was last updated, and a menu on the title with rename, theme, copy link, version history, and delete. Visitors without access are asked to sign in (or told the artifact has not been shared with them, if they are signed in already).
Artifact pages render in a sandboxed frame on a separate origin, so a page written by an agent can never act on your Gatana session.
## Enabling Artifacts
Agents can only create artifacts when the built-in **Artifacts** server is installed, enabled, and visible to them:
* **New organizations** have it installed and shared with the whole organization from the start.
* **Existing organizations** add it themselves: an owner clicks **Add Server**, chooses **Built-in** and picks **Artifacts**. The owner decides who may create artifacts: set the server's visibility to organization, or keep it private and attach chosen users and teams.
The **Artifacts** page in the dashboard tells owners when the server still needs one of these steps. Creating an artifact needs a caller with a user identity, so a connection that does not identify a user cannot create one.
Encrypted at rest like everything else in your organization: artifact content is stored under your organization's key, see [Confidentiality](/confidentiality).
# Audit Logs
A record of everything that happens in your organization, from sign-ins and configuration changes to every MCP call
## Introduction
To get these records into Splunk, Falcon LogScale, Elastic or Sentinel, see the guide [How to get audit logs of every MCP tool call into your SIEM](https://www.gatana.ai/guides/mcp-audit-logs-to-siem).
Gatana keeps an audit log of everything that happens in your organization:
* every MCP request, including each tool call
* every sign-in
* every change to a resource: servers, credentials, profiles, teams, skills, artifacts, secret stores, and organization settings
Each record names the identity behind the event: the user, the profiles that were active, and the credential the call came through, whether an OAuth client or a personal access token. This lets you answer "what did this client do?" even when several clients act as the same user.
Open **Audit Logs** in the left sidebar to read the log. Organization owners see all records; everyone else sees their own activity.
## Logging Detail
How much an MCP call records is set by the MCP audit log level in your organization settings:
| Level | What a call records |
| --------------------- | ----------------------------------------------------- |
| `off` | MCP calls are not recorded |
| `terse` (default) | The server, the tool, the duration, and who called it |
| `detailed` | Adds the call arguments |
| `detailed-with-error` | Adds the response of failed calls |
| `verbose` | Adds the response of every call |
Sign-ins and resource changes are always recorded, whatever the level.
**Omit MCP Protocol Messages** is *On* by default in the organization settings. It excludes `initialize`, `server/discover`, `ping`, `tools/list`, `notifications/*` and `subscriptions/*` from the log. Turn this setting off to log these messages too. Note that at the verbose loging level, each `tools/list` record includes the full tool list which sometimes can we very large.
## Outcome of a Call
Every tool call record (`tools/call`) carries an `outcome`, at every level including `terse`. It says how the call ended in one word, so a filter or a SIEM rule can find the calls that matter without reading the response. Records of protocol messages, such as `initialize` and `tools/list`, have no `outcome`:
| `outcome` | Meaning |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ok` | The tool ran and returned a result |
| `denied` | Gatana refused the call because a policy said no. `reason` names the policy |
| `not-found` | The name matched no server or tool the caller could see. Disabled servers and tools count as not found |
| `needs-credentials` | The policy allowed the call, but no credential exists yet for this user, profile or server |
| `credentials-rejected` | The provider no longer accepted the stored login, for example because it was revoked or expired. The user or an administrator must authorize the server again |
| `upstream-error` | The tool ran and reported an error, or the server could not be reached |
| `invalid-arguments` | Gatana checked the arguments against the tool's schema and they did not match. The tool did not run. Only calls from the web app are checked this way |
| `failed` | The call did not complete for another reason: an internal failure, a [firewall](/tool-firewall) rule that could not be evaluated, or a connection that closed early |
| `unauthenticated` | The request carried no usable credential and never reached a tool |
A `denied` record also carries a `reason`:
| `reason` | Which policy refused the call |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `server-access` | The server is not shared with the user or any of their teams |
| `server-scope` | The token is limited to certain servers, and this server is not one of them |
| `profile-tool` | The profiles bound to the caller exclude this tool |
| `read-only` | The token may only read, and the tool does not declare itself read-only |
| `firewall` | A [firewall](/tool-firewall) rule of the server denied the call. The rule's own `firewall_deny` event names the condition that matched |
| `protection-check-arguments` | A [protection](/tool-firewall#protections) found a match in the arguments, such as a secret, and its action is Block. The tool did not run |
| `protection-check` | A [protection](/tool-firewall#protections) found a match in the result and its action is Block. Gatana withheld the result. Only this denial reason means the tool ran and may have changed data |
`isError` keeps its own meaning: the tool ran and failed, or the connection broke. A denied call is `outcome: "denied"` with `isError: false`, because Gatana answered the client with an error of its own rather than running the tool. To see every call that Gatana stopped, filter on **Outcome is denied**.
A record with `outcome: "ok"` and `isSocketError: true` means that the tool ran and returned a result, but the connection closed before the client received all of it. The outcome stays `ok` because the tool did its work: if it changed data, that change happened.
Code mode is covered too. Code that runs in the sandbox can only call the tools its caller may see; a name outside that set fails inside the sandbox and never reaches a tool. Gatana still records each such name as a tool call of its own, with `mode: "codemode"` and an `outcome` of `denied` (with the `reason`) or `not-found`, under the same user and credential as the `execute_code` call that ran the code. So a script that reaches for a tool its caller was not given leaves the same trace as a direct call would, even when the script catches the error.
## Filtering
The list opens on the last 7 days. Use the date control for other ranges. **Add filter** builds a condition from a field, a comparison, and a value. The comparisons are **is**, **is not**, and **contains**. Date fields use **is after** and **is before**.
You can filter by event name, resource type, outcome, denial reason, protection, protection action, user, OAuth client, access token, server, tool, profile, team, credential, and secret store. Select known values from a list, or enter a value. **is not** matches only records that have the field. For example, **Outcome is not ok** returns tool calls with an outcome other than `ok`. It excludes records without an outcome.
A record must match all conditions. Filters stay in the page URL, so you can bookmark or share a view.
**Protection** and **Protection action** filter the [protection](/tool-firewall#protections) matches of a tool call. They support only **is** and **is not**. **Protection is Secrets** returns calls with a secrets match. **Protection is not Secrets** returns calls where another protection matched and the secrets check did not. Both exclude calls without protection matches.
Conditions can match different protections in the same call. For example, **Protection is Personal data** with **Protection action is Block** also returns a call where Secrets blocked and Personal data only audited.
The same conditions work on the audit log endpoints of the [public API](/public-api-introduction), as a repeated query parameter of the form `filter=field:operator:value`:
```
GET /api/v1/audit-logs?filter=clientId:eq:CLIENT_ID&filter=toolName:contains:search
```
The fields are `eventName`, `entityType`, `entityId`, `userId`, `date`, `clientId`, `patId`, `serverSlug`, `toolName`, `outcome`, `reason`, `protectionCheck`, `protectionAction`, `profileId`, `teamId`, `credentialId`, and `secretStoreId`. `protectionCheck` takes `secrets`, `pii` and `injection`. `protectionAction` takes `audit`, `redact` and `block`. For example, `filter=protectionCheck:eq:secrets` returns the calls with a secrets match.
Each list record contains summary fields in `details`, such as `toolName`, `outcome`, and `reason`. These show what was called, who called, and the result. To read arguments, responses, or the previous and new resource values, get the record by ID:
```
GET /api/v1/audit-logs/RECORD_ID
```
To export complete records in bulk, use [SIEM streaming](/siem-streaming).
## Retention
Gatana Cloud keeps audit records for 365 days. Nightly cleanup removes older records. To keep records longer, use [SIEM streaming](/siem-streaming).
Self-hosted installs keep records for 180 days by default. Set `auditLogs.maxAgeDays` to change the period, or to `0` to keep all records. See [On-Premise & Self-Hosting](/on-premise-self-hosting#configuration).
## Streaming to Your SIEM
To push audit events to a security platform you control, with your own retention, dashboards, and detection rules, see [SIEM Streaming](/siem-streaming).
# Code Mode
Give your agents thousands of tools in only 1'000 tokens
Listing tools can often end up consuming a significant amount of tokens, due to large number of tools and/or overly long descriptions and complex schema definitions. Code Mode replaces all your tools with only two (2): `search_tools` and `execute_code`:
* `search_tools(query)`: Returns the tools and their schema matching `query`.
* `execute_code(code)`: Executes TypeScript/JavaScript code in a isolated virtual machine, the tools returned from `search_tools` are available under `codemode.*`.
**Conceptual Example**
```javascript
const result = await client.callTool('execute_code', { code:`
// Chain multiple operations in one request
const [ pr, comments, reviews ] = await Promise.all([
codemode.github.getPullRequest({ owner: 'microsoft', repo: 'vscode', pull_number: 1234 }),
codemode.github.getPullRequestComments({ owner: 'microsoft', repo: 'vscode', pull_number: 1234 }),
codemode.github.getPullRequestReviews({ owner: 'microsoft', repo: 'vscode', pull_number: 1234 })
]);
// Process data efficiently in-sandbox
return {
title: pr.title,
commentCount: comments.length,
approvals: reviews.filter(r => r.state === 'APPROVED').length
};
`});
```
### Actual Response Content
Calling `search_tools` with `"linear"`:
```markdown
# linearDash.getDiff(urlOrId)
Description:
Exact lookup for a Linear diff. Use with review URLs, GitHub PR URLs, Linear full identifiers, UUIDs, or slugs.
Arguments: urlOrId(required, type=string, description: "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL", minLength=1)
Returns: unknown — log the result to see its shape
-
# linearDash.getDiffThreads(orderBy, urlOrId, resolved, threadId)
Description:
Exact lookup for diff threads. Use with review URLs, GitHub PR URLs, Linear full identifiers, UUIDs, or slugs.
Arguments: orderBy(optional, type=string, description: "Sort: createdAt | updatedAt", enum=[createdAt, updatedAt]), urlOrId(required, type=string, description: "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL", minLength=1), resolved(optional, type=boolean, description: "Filter returned threads by resolved state"), threadId(optional, type=string, description: "Optional top-level thread/comment ID to return")
Returns: unknown — log the result to see its shape
-
(...snip)
```
Using `search_tools` for with *no query* returns a summary:
```markdown
# Servers (e.g. codemode.SERVER)
linear
gmail
gdrive
compression
fetch
codemode
supabase
# Tools (e.g. codemode.SERVER.TOOL) (first 20)
linear.createAttachment
linear.createAttachmentFromUpload
linear.createIssueLabel
(...)
linear.listDiffs
linear.listDocuments
... and 34 more
Use search_tools with a query to display more details (e.g. input schema)
```
## Enabling Code Mode
You can enable Code Mode explicitly by either using a profile, appending the query-string `?codemode` to the MCP URL, or specifying *auto-mode* in your organization settings, so that code mode automatically is used if the number of tools exceed a certain threshold.
### Automatic Code-Mode
You can configure a tool count threshold, which will enable Code Mode if the number of available tools exceed it. You may set the threshold to zero (0) to always use code mode.
1. In **Gatana**, navigate to **Settings**
2. Scroll down to **Features**
3. Check **Automatic Code Mode**
4. *Optional:* Adjust the threshold value
5. *Optional:* Set the threshold value to `0` to always use code mode
### Forcing with Profiles
First, you need a profile which has *force code mode* enabled:
1. In **Gatana**, navigate to **Profiles**
2. Either create a new or edit an existing **Profile**
3. In the modal, select **Force Code Mode**
4. Click on **Save**
Whenever this profile is used in a request, code mode will be enabled for *all servers*, not only the ones covered in the profile.
### Query-String codemode
Normally you would connect to: `https://gatana.ai/mcp`. To force code-mode, you can instead use either:
* `https://gatana.ai/mcp?codemode`
* `https://gatana.ai/mcp?codemode=true`
## Read More
* [https://blog.cloudflare.com/code-mode-mcp/](https://blog.cloudflare.com/code-mode-mcp/)
* [https://blog.cloudflare.com/code-mode/](https://blog.cloudflare.com/code-mode/)
* [https://github.com/universal-tool-calling-protocol/code-mode](https://github.com/universal-tool-calling-protocol/code-mode)
## Environment and Isolation
The code is executed in an isolated (per-organization) virtual machine using [Kata Containers](https://katacontainers.io/), which provides a very strong layer of security and isolation.
## Access and Audit
Code in the sandbox can only call the tools its caller may see: the same server shares, profiles and token scopes apply as for a direct tool call. A name outside that set fails inside the sandbox, and Gatana records each such name as a refused tool call in the [audit log](/audit-logs#outcome-of-a-call), even when the code catches the error.
# Compression
Reduce tool response tokens with up to 99% with automatic output compression
Listing tools can often end up consuming a significant amount of tokens, due to large number of tools and/or overly long descriptions and complex schema definitions. Code Mode replaces all your tools with only two (2): `search_tools` and `execute_code`.
See [Code Mode](/code-mode) for more information.
Every MCP tool call returns raw data into your AI client's context window. A Playwright snapshot costs 56 KB. Twenty GitHub issues cost 59 KB. One access log — 45 KB. After a few tool calls, a significant portion of your context budget is gone — filled with data the model may never reference again.
This is an inherent tension in MCP: tools need to return useful data, but AI clients have finite context windows. The more tools you use, the faster you run out of room for actual reasoning.
Compression aims to solve this by:
1. **Compression:** Compression is applied transparently to `text` and `structuredContent`
2. **Indexing:** The content is indexed
3. **Search:** A tool is used to search the index and retrieve the desired result.
Binary content types (e.g. images, audio) are passed through unchanged.
## Agentic
These benchmarks were measured using the following prompt to Claude Sonnet 4.6.
The "Search Results" column shows data returned by `compression_search` when the agent needed to retrieve specific details from the compressed output. In many cases (like `list_issues`) the compressed summary already contains enough information and no search is needed.
### Research GitHub Project (74%)
> Research [https://github.com/modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) — architecture, tech stack, top contributors, open issues, and recent pull requests
| Tool | Without Compression | Compressed Summary | Search Results | Total With Compression | Reduction |
| --------------------- | ------------------: | -----------------: | -------------: | ---------------------: | --------: |
| `list_pull_requests` | 253 KiB | 1.3 KiB | 78 KiB | 79.3 KiB | 68.7% |
| `list_commits` | 50 KiB | 864 B | 693 B | 1.5 KiB | 97.0% |
| `get_repository_tree` | 41 KiB | 3.5 KiB | 9.7 KiB | 13.2 KiB | 67.8% |
| `list_issues` | 26 KiB | 1.5 KiB | — | 1.5 KiB | 94.2% |
| **Total** | **370 KiB** | | | **95.5 KiB** | **74.2%** |
### JSON lookup (99%)
> what is the status of task T44812
> [https://sample.json-format.com/api/download?url=https%3A%2F%2Ffiles.jsons.live%2Femployees%2F5-level%2F500-KB%2Fminified.json\&name=500%20KB%205%20Level%20Minified.json](https://sample.json-format.com/api/download?url=https%3A%2F%2Ffiles.jsons.live%2Femployees%2F5-level%2F500-KB%2Fminified.json\&name=500%20KB%205%20Level%20Minified.json)
| Tool | Without Compression | Compressed Summary | Search Results | Total With Compression | Reduction |
| ------------------ | ------------------: | -----------------: | -------------: | ---------------------: | --------: |
| `web` (fetch page) | 501 KiB | 706 B | 5.4 KiB | 6.1 KiB | 98.8% |
| **Total** | **501 KiB** | | | **6.1 KiB** | **98.8%** |
## Content Types
Below are examples from simple tool calls, without considering agentic look and multi-turn thinking.
### Text (96%)
This is the output from fetching the original RFC for the HTTP protocol ([https://datatracker.ietf.org/doc/html/rfc8693](https://datatracker.ietf.org/doc/html/rfc8693)). The output was compressed by 95%. Note that the accuracy of the section extraction depends on the effectiveness of the HTML-to-Markdown processor in the `fetch` tool.
```
---------------------
[Output compressed: 71.2KB → 2.7KB. 20 sections in 54 chunks indexed. Reference ID: csrc_r14OG2P2]
Use "compression_search" to search the full output with source_id "csrc_r14OG2P2" to retrieve all chunks.
Indexed sections:
- RFC 8693
- OAuth 2.0 Token Exchange
- OAuth 2.0 Token Exchange > Abstract
- OAuth 2.0 Token Exchange > Status of This Memo
- OAuth 2.0 Token Exchange > Copyright Notice
- OAuth 2.0 Token Exchange > Table of Contents
- OAuth 2.0 Token Exchange > 1.Introduction
- OAuth 2.0 Token Exchange > 1.Introduction > 1.1.Delegation vs. Impersonation Semantics
- OAuth 2.0 Token Exchange > 1.Introduction > 1.2.Requirements Notation and Conventions
- OAuth 2.0 Token Exchange > 1.Introduction > 1.3.Terminology
- OAuth 2.0 Token Exchange > 2.Token Exchange Request and Response
- OAuth 2.0 Token Exchange > 2.Token Exchange Request and Response > 2.1.Request
- OAuth 2.0 Token Exchange > 2.Token Exchange Request and Response > 2.1.Request > 2.1.1.Relationship between Resource, Audience, and Scope
- OAuth 2.0 Token Exchange > 2.Token Exchange Request and Response > 2.2.Response
- OAuth 2.0 Token Exchange > 2.Token Exchange Request and Response > 2.2.Response > 2.2.1.Successful Response
- OAuth 2.0 Token Exchange > 2.Token Exchange Request and Response > 2.2.Response > 2.2.2.Error Response
- OAuth 2.0 Token Exchange > 2.Token Exchange Request and Response > 2.3.Example Token Exchange
- OAuth 2.0 Token Exchange > 3.Token Type Identifiers
- OAuth 2.0 Token Exchange > 4.JSON Web Token Claims and Introspection Response Parameters
- OAuth 2.0 Token Exchange > 4.JSON Web Token Claims and Introspection Response Parameters > 4.1."act" (Actor) Claim
... and 29 more
```
We use the `compression_search` tool to find information about refresh tokens:
```
## refresh token
--- [web] SOURCE=csrc_r14OG2P2 | CHUNK=chnk_Q0RhS0987fu1JxfZ5NRky ---
### OAuth 2.0 Token Exchange > 3.Token Type Identifiers
## 3.Token Type Identifiers
Several parameters in this specification utilize an identifier as the value to
describe the token in question. Specifically, they are the `requested_token_type`, `subject_token_type`, and `actor_token_type` parameters of the request and the `issued_token_type` member of the response. Token type identifiers are URIs. Token exchange can
work with both tokens issued by other parties and tokens from the given
authorization server. For the former, the token type identifier indicates the syntax (e.g.,
JWT or SAML 2.0) so the authorization server can parse it; for the latter, it
indicates what the given authorization server issued it for (e.g., `access_token` or `refresh_token`).¶
The following token type identifiers are defined by this specification. Other
URIs MAY be used to indicate other token types.¶
- **urn:ietf:params:oauth:token-type:access_token** Indicates that the token is an OAuth 2.0 access token issued by the given
authorization server.¶
- **urn:ietf:params:oauth:token-type:refresh_token** Indicates that the token is an OAuth 2.0 refresh token issued by the given
authorization server.¶
- **urn:ietf:params:oauth:token-type:id_token** Indicates that the token is an ID Token as defined in Section 2 of [OpenID.Core].¶
- **urn:ietf:params:oauth:token-type:saml1** Indicates that the token is a base64url-encoded SAML 1.1 [OASIS.saml-core-1.1] assertion.¶
- **urn:ietf:params:oauth:token-type:saml2** Indicates that the token is a base64url-encoded SAML 2.0 [OASIS.saml-core-2.0-os] assertion.¶
The value `urn:ietf:params:oauth:token-type:jwt`, which is defined in [Section 9](https://www.rfc-editor.org/rfc/rfc7519#section-9) of [JWT], indicates that the token is a JWT.¶
The distinction between an access token and a JWT is subtle. An access token
represents a delegated authorization decision, whereas JWT is a token format. An
access token can be formatted as a JWT but doesn't necessarily have to be. And a
JWT might well be an access token, but not all JWTs are access tokens. The
intent of this specification is that `urn:ietf:params:oauth:token-type:access_token` be an indicator that the token is a typical OAuth access token issued by the
authorization server in question, opaque to the client, and usable the same manner
as any other access token obtained from that authorization server. (It could
well be a JWT, but the client isn't and needn't be aware of that fact.) Whereas, `urn:ietf:params:oauth:token-type:jwt` is to indicate specifically that a JWT is being requested or sent (perhaps in a
cross-domain use case where the JWT is used as an authorization grant to obtain
an access token from a different authorization server as is facilitated by [RFC7523]).¶
Note that for tokens that are binary in nature, the URI used for conveying them
needs to be associated with the semantics of a base64 or other encoding
suitable for usage with HTTP and OAuth.¶
```
### JSON (99%)
This is the Gatana output form calling GitHub's `list_pull_requests` on [https://github.com/modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers). Immidiately we can see the pull requests and some metadata.
```
---------------------
[Output compressed: 491.1KB → 2.3KB. 20 sections in 154 chunks indexed. Reference ID: csrc_QyYCiutu]
Use "compression_search" to search the full output with source_id "csrc_QyYCiutu" to retrieve all chunks.
Indexed sections:
- feat: add Node API — machine-native GIS processing MCP server with x402 payments | state: open | created_at: 2026-03-05T00:48:40Z
- Add GEOScore MCP server | state: open | created_at: 2026-03-04T23:38:04Z
- Add ProofBets MCP Server to community servers | state: open | created_at: 2026-03-04T22:18:53Z
- Add evc-team-relay-mcp - Obsidian vault MCP server via self-hosted Team Relay | state: open | created_at: 2026-03-04T21:59:34Z
- Add LegacyShield Zero-Knowledge Vault to third-party servers | state: open | created_at: 2026-03-04T21:37:49Z
- Add WAzion to official integrations list | state: open | created_at: 2026-03-04T15:58:28Z
- Add senior-design-director-mcp — design director intelligence for Cla… | state: open | created_at: 2026-03-03T17:17:30Z
- Add PowerSun — TRON Energy MCP Server (27 tools, remote) | state: open | created_at: 2026-03-02T19:40:08Z
- Update README.md | state: open | created_at: 2026-03-02T13:16:21Z
- Add YourMemory — Ebbinghaus-based persistent memory MCP server | state: open | created_at: 2026-03-02T11:41:51Z
- Create DC Hub - Data Center Intelligence | state: open | created_at: 2026-03-02T08:48:33Z
- Add csvglow to Community Servers | state: open | created_at: 2026-03-02T04:02:44Z
- Add Mixpeek MCP Server | state: open | created_at: 2026-03-01T21:57:17Z
- Add Vault MCP to community servers | state: open | created_at: 2026-03-01T17:57:55Z
- Add strict ACL startup mode for filesystem, fetch, and git servers | state: open | created_at: 2026-03-01T16:49:00Z
- Add mcp-architector to community servers list | state: open | created_at: 2026-03-01T15:26:55Z
- Add javaperf to community servers list | state: open | created_at: 2026-03-01T15:04:51Z
- Add MoldSim MCP to Community Servers | state: open | created_at: 2026-03-01T14:56:43Z
- fix(filesystem): ensure bare Windows drive letters normalize to root | state: open | created_at: 2026-03-01T10:59:33Z
- Add Knowfun MCP to community servers | state: open | created_at: 2026-03-01T04:07:40Z
```
However, if we want more details, here is the search output for `feat: add Node API — machine-native GIS processing MCP server with x402 payments`.
```
## feat: add Node API — machine-native GIS processing MCP server with x402 payments
--- [list_pull_requests] csrc_QyYCiutu ---
### feat: add Node API — machine-native GIS processing MCP server with x402 payments | state: open | created_at: 2026-03-05T00:48:40Z
id: 3355984687, number: 3471, state: open, locked: false, title: feat: add Node API — machine-native GIS processing MCP server with x402 payments, body: ## Node API
**Repository:** https://github.com/eianray/node-api
**Remote MCP endpoint:** https://nodeapi.ai/mcp/sse
**Website:** https://nodeapi.ai
### What it does
Node API is a machine-native spatial data processing API built for AI agents. It exposes GIS operations (format conversion, CRS reprojection, geometry validation, clipping, DXF/CAD extraction, topology operations) as clean REST endpoints.
### Why it belongs here
- Full MCP server with 12 tools covering spatial data operations
- Remote SSE endpoint — agents connect directly, no local install required
- x402/USDC micropayment support on Base — agents can pay autonomously
- No accounts, no API keys, no subscriptions
- Handles formats incumbents won't touch: Esri .gdb, DXF/CAD
### Tools
`meridian_convert`, `meridian_reproject`, `meridian_validate`, `meridian_repair`, `meridian_schema`, `meridian_clip`, `meridian_dxf`, `meridian_buffer`, `meridian_union`, `meridian_intersect`, `meridian_difference`, `meridian_pricing`, created_at: 2026-03-05T00:48:40Z, updated_at: 2026-03-05T00:48:40Z, user.login: eianray, user.id: 13755964, user.node_id: MDQ6VXNlcjEzNzU1OTY0, user.avatar_url: https://avatars.githubusercontent.com/u/13755964?v=4, user.html_url: https://github.com/eianray, user.type: User, user.site_admin: false, user.url: https://api.github.com/users/eianray, user.events_url: https://api.github.com/users/eianray/events{/privacy}, user.following_url: https://api.github.com/users/eianray/following{/other_user}, user.followers_url: https://api.github.com/users/eianray/followers, user.gists_url: https://api.github.com/users/eianray/gists{/gist_id}, user.organizations_url: https://api.github.com/users/eianray/orgs, user.received_events_url: https://api.github.com/users/eianray/received_events, user.repos_url: https://api.github.com/users/eianray/repos, user.starred_url: https://api.github.com/users/eianray/starred{/owner}{/repo}, user.subscriptions_url: https://api.github.com/users/eianray/subscriptions, draft: false, url: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3471, html_url: https://github.com/modelcontextprotocol/servers/pull/3471, issue_url: https://api.github.com/repos/modelcontextprotocol/servers/issues/3471, statuses_url: https://api.github.com/repos/modelcontextprotocol/servers/statuses/1d2b04c10117f47acd4091eb6393e2b7297f8d0d, diff_url: https://github.com/modelcontextprotocol/servers/pull/3471.diff, patch_url: https://github.com/modelcontextprotocol/servers/pull/3471.patch, commits_url: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3471/commits, comments_url: https://api.github.com/repos/modelcontextprotocol/servers/issues/3471/comments, review_comments_url: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3471/comments, review_comment_url: https://api.github.com/repos/modelcontextprotocol/servers/pulls/comments{/number}, author_association: NONE, node_id: PR_kwDONRaG_87ICEMv, merge_commit_sha: eb5e80f85012d5c8ccecf5b72a231b084758d57d, _links.self.href: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3471, _links.html.href: https://github.com/modelcontextprotocol/servers/pull/3471, _links.issue.href: https://api.github.com/repos/modelcontextprotocol/servers/issues/3471, _links.comments.href: https://api.github.com/repos/modelcontextprotocol/servers/issues/3471/comments, _links.review_comments.href: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3471/comments, _links.review_comment.href: https://api.github.com/repos/modelcontextprotocol/servers/pulls/comments{/number}, _links.commits.href: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3471/commits, _links.statuses.href: https://api.github.com/repos/modelcontextprotocol/servers/statuses/1d2b04c10117f47acd4091eb6393e2b7297f8d0d, head.label:
--- [list_pull_requests] csrc_QyYCiutu ---
### feat: add Node API — machine-native GIS processing MCP server with x402 payments | state: open | created_at: 2026-03-05T00:48:40Z
github.com/repos/modelcontextprotocol/servers/issues/comments{/number}, base.repo.issue_events_url: https://api.github.com/repos/modelcontextprotocol/servers/issues/events{/number}, base.repo.issues_url: https://api.github.com/repos/modelcontextprotocol/servers/issues{/number}, base.repo.keys_url: https://api.github.com/repos/modelcontextprotocol/servers/keys{/key_id}, base.repo.labels_url: https://api.github.com/repos/modelcontextprotocol/servers/labels{/name}, base.repo.languages_url: https://api.github.com/repos/modelcontextprotocol/servers/languages, base.repo.merges_url: https://api.github.com/repos/modelcontextprotocol/servers/merges, base.repo.milestones_url: https://api.github.com/repos/modelcontextprotocol/servers/milestones{/number}, base.repo.notifications_url: https://api.github.com/repos/modelcontextprotocol/servers/notifications{?since,all,participating}, base.repo.pulls_url: https://api.github.com/repos/modelcontextprotocol/servers/pulls{/number}, base.repo.releases_url: https://api.github.com/repos/modelcontextprotocol/servers/releases{/id}, base.repo.stargazers_url: https://api.github.com/repos/modelcontextprotocol/servers/stargazers, base.repo.statuses_url: https://api.github.com/repos/modelcontextprotocol/servers/statuses/{sha}, base.repo.subscribers_url: https://api.github.com/repos/modelcontextprotocol/servers/subscribers, base.repo.subscription_url: https://api.github.com/repos/modelcontextprotocol/servers/subscription, base.repo.tags_url: https://api.github.com/repos/modelcontextprotocol/servers/tags, base.repo.trees_url: https://api.github.com/repos/modelcontextprotocol/servers/git/trees{/sha}, base.repo.teams_url: https://api.github.com/repos/modelcontextprotocol/servers/teams, base.repo.visibility: public, base.user.login: modelcontextprotocol, base.user.id: 182288589, base.user.node_id: O_kgDOCt2AzQ, base.user.avatar_url: https://avatars.githubusercontent.com/u/182288589?v=4, base.user.html_url: https://github.com/modelcontextprotocol, base.user.type: Organization, base.user.site_admin: false, base.user.url: https://api.github.com/users/modelcontextprotocol, base.user.events_url: https://api.github.com/users/modelcontextprotocol/events{/privacy}, base.user.following_url: https://api.github.com/users/modelcontextprotocol/following{/other_user}, base.user.followers_url: https://api.github.com/users/modelcontextprotocol/followers, base.user.gists_url: https://api.github.com/users/modelcontextprotocol/gists{/gist_id}, base.user.organizations_url: https://api.github.com/users/modelcontextprotocol/orgs, base.user.received_events_url: https://api.github.com/users/modelcontextprotocol/received_events, base.user.repos_url: https://api.github.com/users/modelcontextprotocol/repos, base.user.starred_url: https://api.github.com/users/modelcontextprotocol/starred{/owner}{/repo}, base.user.subscriptions_url: https://api.github.com/users/modelcontextprotocol/subscriptions
--- [list_pull_requests] csrc_QyYCiutu ---
### Add Knowfun MCP to community servers | state: open | created_at: 2026-03-01T04:07:40Z
id: 3339950417, number: 3433, state: open, locked: false, title: Add Knowfun MCP to community servers, body: - Added Knowfun MCP server for AI-powered content generation
- Features: educational courses, visual posters, interactive games, and short films
- Available via NPM: npm install -g knowfun-mcp
- GitHub: https://github.com/MindStarAI/KnowFun-MCP
## Description
## Publishing Your Server
**Note: We are no longer accepting PRs to add servers to the README.** Instead, please publish your server to the [MCP Server Registry](https://github.com/modelcontextprotocol/registry) to make it discoverable to the MCP ecosystem.
To publish your server, follow the [quickstart guide](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx). You can browse published servers at [https://registry.modelcontextprotocol.io/](https://registry.modelcontextprotocol.io/).
## Server Details
- Server:
- Changes to:
## Motivation and Context
## How Has This Been Tested?
## Breaking Changes
## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
## Checklist
- [x] I have read the [MCP Protocol Documentation](https://modelcontextprotocol.io)
- [x] My changes follows MCP security best practices
- [x] I have updated the server's README accordingly
- [x] I have tested this with an LLM client
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have documented all environment variables and configuration options
## Additional context
, created_at: 2026-03-01T04:07:40Z, updated_at: 2026-03-01T04:08:48Z, user.login: duguyixiaono1, user.id: 2703461, user.node_id: MDQ6VXNlcjI3MDM0NjE=, user.avatar_url: https://avatars.githubusercontent.com/u/2703461?v=4, user.html_url: https://github.com/duguyixiaono1, user.type: User, user.site_admin: false, user.url: https://api.github.com/users/duguyixiaono1, user.events_url: https://api.github.com/users/duguyixiaono1/events{/privacy}, user.following_url: https://api.github.com/users/duguyixiaono1/following{/other_user}, user.followers_url: https://api.github.com/users/duguyixiaono1/followers, user.gists_url: https://api.github.com/users/duguyixiaono1/gists{/gist_id}, user.organizations_url: https://api.github.com/users/duguyixiaono1/orgs, user.received_events_url: https://api.github.com/users/duguyixiaono1/received_events, user.repos_url: https://api.github.com/users/duguyixiaono1/repos, user.starred_url: https://api.github.com/users/duguyixiaono1/starred{/owner}{/repo}, user.subscriptions_url: https://api.github.com/users/duguyixiaono1/subscriptions, draft: false, url: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3433, html_url: https://github.com/modelcontextprotocol/servers/pull/3433, issue_url: https://api.github.com/repos/modelcontextprotocol/servers/issues/3433, statuses_url: https://api.github.com/repos/modelcontextprotocol/servers/statuses/978ec383aeb118087af62c84b4ed8d8fba84b8dd, diff_url: https://github.com/modelcontextprotocol/servers/pull/3433.diff, patch_url: https://github.com/modelcontextprotocol/servers/pull/3433.patch, commits_url: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3433/commits, comments_url: https://api.github.com/repos/modelcontextprotocol/servers/issues/3433/comments, review_comments_url: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3433/comments, review_comment_url: https://api.github.com/repos/modelcontextprotocol/servers/pulls/comments{/number}, author_association: NONE, node_id: PR_kwDONRaG_87HE5lR, merge_commit_sha: 17e313b0787a4dd9bf87d7a11b601c3c32ee687e, _links.self.href: https://api.github.com/repos/modelcontextprotocol/servers/pulls/3433, _links.html.href:
```
Often agents do not strictly need the exact JSON representation, but rather require some information *inside* the JSON. Gatana transforms any JSON in `structuredOutput` to a representation which is more efficient for the our model to work with.
If this is not acceptable, JSON compression can be disabled on a per-server basis. This will disable compression completely for a request if the tool result contains structured content (JSON). You can also disable compression on a per-request basis, see [Controlling](#controlling) below.
### Other Content Types
Compression is completely disabled for any request and the original content is passed through verbatim, if any non-JSON or non-text content type is present.
## Compression
With compression is meant the splitting of the original content into several small chunks, ideally with a summary, extracted from e.g. a Markdown header. These chunks are the units will be returned later when searching through the output.
Gatana automatically detects the data type of the tool output and picks the an approriate compression method:
| Content | Method |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Markdown** | Split by heading hierarchy (H1–H4), preserving code blocks as atomic units and building hierarchical section titles like `"API Reference > Authentication > OAuth Flow"`. |
| **Plain text** | Split by blank lines into natural sections. If that doesn't produce reasonable chunks, falls back to fixed-size groups of 20 lines with overlap. |
| **JSON** | Parsed and flattened into natural-language text. Arrays of objects produce one chunk per item. Nested objects become dot-path key-value pairs (e.g. `server.host: prod-01`). |
### Indexing
Since no single search method works well for all queries and data, several different methods are combined their result merged using Reciprocal Rank Fusion (RRF).Fuzzy matching corrects typos but ignores meaning. Vector search captures semantics but is imprecise on exact names. Gatana combines four complementary layers and
| Layer | Scenario |
| ---------- | ---------------------------------------------------------------------------------- |
| Stemming | `"caching"` matches `"cached"`, `"caches"`, `"cache"` |
| Substring | `"useEff"` finds `"useEffect"`, `"authenticat"` finds `"authentication"` |
| Similarity | `"kuberntes"` matches `"kubernetes"`, `"autentication"` matches `"authentication"` |
| Semantic†‡ | `"how to authenticate"` matches a chunk about `"OAuth 2.0 token exchange"` |
† The semantic meaning is extracted using an AI model running exclusively on Gatana infrastructure. Your data ever leaves our infrastructure.
‡ The embedding is done asynchronously
### Searching
Gatana provides build-in tools available in the Gateway to find the desired output:
| Tool | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `search` | Search across all compressed outputs. Accepts an **array of queries** in one call, with optional filters by source tool or source ID. |
| `list_indexed_results` | List all compressed outputs from the last hour, with section titles and sizes. |
| `list_chunk_titles` | List all section titles for a specific `source_id` |
| `get_chunk_span` | Retrieve a span of chunks adjacent to a specific chunk by its reference ID. Use this when you need to see the context around a particular chunk. |
| `get_full_content` | Retrieve all chunks of a compressed output for a specific `source_id`, in order. Returns the complete indexed content, so it can be large. |
| `get_original_content` | Retrieve the verbatim original tool output for a specific `source_id`, exactly as the tool returned it before compression. |
## Controlling
Output compression has three levels of configuration:
| Level | Setting | Description |
| ---------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------- |
| **Organization** | **Allow Output Compression** | Master switch that enables compression for the entire organization |
| **Organization** | **Semantic Compression** | Enables semantic search across compressed outputs (uses a local model in Gatana infrastructure) |
| **Server** | **Enable Output Compression** | Toggle compression on or off per server |
| **Server** | **Compression Threshold** | Minimum response size before compression activates (default: 8192 bytes, minimum: 1024 bytes) |
| **Request** | `_meta["ai.gatana/no-compression"]` | Set to `true` on a `tools/call` request to skip compression for that call |
Responses smaller than the threshold pass through unchanged. Error responses are never compressed.
## Confidentiality & Expiry
Compressed outputs are scoped by user and are ephemeral. They expire after **1 hour** and are automatically cleaned up.
## Credit
This feature is inspired by Mert Köseoğlu's [claude-context-mode](https://github.com/mksglu/claude-context-mode)
# Confidentiality & Privacy
How Gatana Cloud protects your confidential and private data
**On-Premise & Self-Hosting**
For complete data-ownership, you can self-host Gatana, see [On-Premise & Self-Hosting](/on-premise-self-hosting) for more information.
#### Gatana Trust Center
See our progress towards full compliance: [https://gatana.auditbadger.com/trust](https://gatana.auditbadger.com/trust)
## Overview
Gatana Cloud protects all sensitive customer data using Google Cloud Key Management Service (KMS) and envelope encryption. Credentials, API keys, and all other sensitive information remain protected even in the event of a database breach. Additionally the database is encrypted-at-rest on disk using LUKS2, AES-XTS.
With envelope encryption each organization creates its own encryption key on sign-up. This key is used to encrypt all sensitive information for this organization, providing complete cryptographic separation between organizations.
For additional isolation, [secret stores](/secret-stores) allow you to store credentials in your own vault and have Gatana retrieve them at runtime.
## Envelope Encryption
Gatana uses envelope encryption to protect sensitive data. Each organization is assigned a unique AES-256-GCM data encryption key (DEK) that encrypts all sensitive information within that organization. The DEK itself is encrypted by Google Cloud KMS and stored in this protected form. At runtime, Gatana decrypts the DEK using KMS and retains it in memory only for the duration required to perform cryptographic operations.
## Self-hosted installs
Self-hosted installs choose which key manager wraps the DEK, with the Helm value `kms.provider`:
| `kms.provider` | What wraps the DEK | Also set |
| ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------- |
| `gcp` | Google Cloud KMS | `kms.gcpKeyId` |
| `aws` | AWS KMS | `kms.aws.keyId` (key ARN, `alias/`, or key ID) |
| `none` (default) | Nothing. Sensitive values are stored as-is, protected only by disk encryption | |
For AWS, the backend needs `kms:Encrypt` and `kms:Decrypt` on the key. It picks up credentials from the
usual chain (IRSA, EKS Pod Identity, or the node role). If those credentials belong to something else,
such as SES, give the KMS client its own keys through `kms.aws.credentialsSecret`. A key given as a full
ARN also names its region, so `kms.aws.region` is only needed for an alias or a bare key ID.
## Questions
If you have questions about our security practices, please contact [support@gatana.ai](mailto:support@gatana.ai).
# Clients & API Keys
See and control everything that can reach your Gatana, from connected OAuth clients to personal access tokens
## Introduction
**Clients & API Keys** in the left sidebar lists everything that can reach Gatana as you, in three tables:
* **OAuth clients**: apps that signed in through [OAuth](/connecting-to-gatana#oauth-21), such as Claude Desktop, Cursor, or the ChatGPT connectors. A client appears here automatically once it has signed in; there is nothing to register.
* **Federated clients**: apps that reach Gatana with an access token from your organization's own identity provider, through [OIDC Access Token Trust](/federation#oidc-access-token-trust). One appears here automatically at its first request.
* **Personal access tokens**: static secrets you create yourself and paste into a client that cannot sign in on its own.
Each is revoked differently: an OAuth client is disconnected, a token is deleted, and a federated client is revoked at your identity provider, since Gatana does not issue its token. Everything on this page is yours alone. Each user sees and manages only their own connections and tokens.
## Connected Clients
Each row shows the client's name and version, what kind of client it is (CLI, desktop app, web app, or trusted IdP token), the [profiles](/profiles) you attached to it, when it last made a request, and when it connected.
A client chooses what to call itself, both when it registers and over MCP. The name is a label for you to recognize the connection by. It never decides what the client can access.
### Renaming a Connection
Click **Edit** on the row and give the connection a name of your own. This is useful for telling two connections from the same app apart, for example Claude Code on your laptop and Claude Code on a server. Leave the name empty to keep showing the name the client reports.
### Attaching Profiles
Click **Edit** on the row and select one or more profiles. The profiles are applied on every request that client makes, on top of the profiles you already hold, just like profiles on a personal access token. See [Applying Profiles](/profiles#applying-profiles) for what a profile can do, including narrowing a client down with a [restrictive profile](/limiting-servers).
### Disconnecting a Client
Click **Revoke** on the row. The client loses access immediately and has to sign in again to come back. Any profiles you attached to it are forgotten.
### Inactive Clients
An OAuth client is marked **Inactive** when it no longer holds a refresh token to renew its access with. It will have to sign in again the next time it connects. Disconnect it if you do not expect it back.
### Federated Clients
A federated client is an app that never signed in through Gatana. It authenticates with an access token issued by your identity provider, which Gatana accepts through [OIDC Access Token Trust](/federation#oidc-access-token-trust). The row is identified by the client ID the token names.
Renaming and attaching profiles work the same as for OAuth clients. There is no **Revoke** button: Gatana did not issue the token, so access is taken away in your identity provider. A federated client is marked **Inactive** after 30 days without a request.
### Seeing What a Client Did
The **Logs** button on a row opens the [audit log](/audit-logs) already filtered to that client, so you can see every call it made.
## Personal Access Tokens
Create a token with **New Token**. Each row lets you reveal and copy the token value, shows the profiles assigned to it, when it was last used, and when it was created.
* **Connect** shows ready-made configuration for common clients (Claude Code, VS Code, Cursor, and others) with the URL and the token already filled in. See [Connecting to Gatana](/connecting-to-gatana#personal-access-token) for the general shape.
* **Logs** opens the [audit log](/audit-logs) filtered to calls made with that token.
* **Edit** renames the token or changes its profiles.
* **Delete** removes the token. Anything still using it stops working at once.
Personal access tokens for service accounts are managed on the account's detail page under **People & Accounts**, by organization owners.
# Connecting to Gatana
Connect Claude Code to Gatana, sign up or sign in from the terminal, and install agent skills and plugins
## Agent Mode
```bash
npx skills add gatana-ai/gatana
```
Then ask your agent:
```
Create a Gatana organization and connect this computer to it
```
or without skill:
```
Follow https://www.gatana.ai/llms.txt and to create a Gatana organization and connect this computer to it
```
Skill source: [https://github.com/gatana-ai/gatana/tree/main/skills/gatana](https://github.com/gatana-ai/gatana/tree/main/skills/gatana).
## Nerd Mode
```bash
curl -fsSL https://github.com/gatana-ai/gatana/releases/latest/download/install.sh | sh
gatana install
```
```bash
brew install gatana-ai/tap/gatana
gatana install
```
This will:
* Install the [Gatana CLI](https://github.com/gatana-ai/gatana/tree/main/packages/gatana-cli)
* Add MCP connection for Claude Code (`~/.claude`), Codex (`~/.codex`), Hermes (`~/.hermes`)
* Add hook for syncing skills
## Without using the CLI
These examples use Claude Code, but any harness which can use MCP servers can be used with Gatana.
### 1. Add as an MCP server
Run this in your terminal:
```bash
claude mcp add --transport http --scope user gatana https://gatana.ai/mcp
claude mcp login gatana
```
### Step 2: Use the tools
Ask Claude Code something that needs a tool, for example "Which tools do you get from Gatana?". Claude Code lists the tools of your organization.
The connection now shows under [Clients & API Keys](/connected-clients) in the left sidebar of Gatana. There you can see when it was last used, open its [audit log](/audit-logs), and disconnect it.
Write `.mcp.json` in the project root and commit it. Every member who opens the project gets Gatana, and signs in with their own account.
```json
{
"mcpServers": {
"gatana": { "type": "http", "url": "https://gatana.ai/mcp" }
}
}
```
## Other clients
The same URL works in Claude, ChatGPT, Codex, Cursor, VS Code and every other MCP client.
## How the sign-in works
### OAuth 2.1
Most clients sign in through your browser, as Claude Code does above. The client registers itself. You set up nothing in Gatana first. The first time an app that is not one of Gatana's own signs in, the browser asks you to approve it. Gatana remembers your answer.
### Personal access token
A client that cannot open a browser, such as a script or a server, uses a personal access token. Create one under [Clients & API Keys](/connected-clients#personal-access-tokens). Send it in the `Authorization` header. A token belongs to one organization, so use it with the URL of your organization:
```json
{
"mcpServers": {
"gatana": {
"type": "http",
"url": "https://YOUR_ORG_ID.gatana.ai/mcp",
"headers": { "Authorization": "Bearer YOUR_PAT_HERE" }
}
}
}
```
Keep the token out of source control. Use OAuth when the client can.
## The Gatana plugin
The Gatana plugin gives Claude Code the tools of your gateway, and the [skills](/skills-fundamentals) of your organization. A skill tells Claude how a type of work is done in your organization: how to deploy a service, how to review a pull request. With the plugin, Claude Code checks the skills at the start of a task and follows the skill that applies.
The plugin replaces Step 1. Run this in Claude Code:
```text
/plugin marketplace add gatana-ai/gatana-plugins
/plugin install gatana@gatana
```
Then run `/mcp`, select `plugin:gatana:gatana`, and sign in as in Step 2.
The same plugin exists for the Claude app, Cowork, ChatGPT and Codex. The install steps are in the [plugin repository](https://github.com/gatana-ai/gatana-plugins).
An admin can download a plugin made for one organization on the **Install Gatana** page, and publish it to the Claude organization or the ChatGPT workspace. That plugin connects to the address of your organization, so members do not choose the organization at sign-in.
# Credentials
Credentials can be configured on a per-server basis and be global or per-user basis.
## Introduction
Gatana splits authorization into two components:
* **Authorization Configuration**: describing what form credentials should have
* **Credentials**: the resolved tokens/values which will be passed when calling the tool
Gatana supports OAuth 2.1 and API keys. It sends credentials to the server as follows:
* **API keys**: Use `{{ credentials.keys.MY_API_KEY }}` in the remote MCP URL (for example, a query parameter), HTTP headers, environment variables, or the command.
* **OAuth access tokens**: Gatana sends the token in the `Authorization` header with the Bearer scheme on each request. There is no template expression for OAuth tokens.
The credentials can be scoped to the server; meaning that all Gatana users share the same credentials when calling the tools, or they can be user-scoped; meaning that each user has their own credentials. Additionally, each profile can have their own set of credentials. See [Profiles](/profiles) for more information
## Tool Refresh Policy
You can control which credentials (if any) should be used for refreshing the tools for a server. The available options are:
* **Server-Scoped Credentials**: Use server-scoped credentials.
* **Any Credentials**: Use any credentials, i.e. default/server-, user- or profile-scoped.
* **No Credentials**: Do not use or require any credentials for tool refreshes. This is useful if the server does not require credentials for tool refreshes (e.g. OpenAPI servers, or any server that don't require authorization for tool listing), but does require them for tool-calling. It can simplify credential management by not requiring server-scoped credentials when user-scoped credentials are required.
## Authorization Configuration
Gatana supports three methods of authorization for servers:
* **No authorization:** the server can always be used without requiring any authorization.
* **OAuth 2.1:** the server requires a bearer token which should be provded using OAuth 2.1.
* **OAuth Client Credentials** the server requires a bearer token which should be provded using OAuth Client Credentials grant.
* **API Keys:** one or more static key-value pairs which can be used in headers, command or environment variables.
Gatana will automatically inject a valid access token in the Authorization header using the
Bearer-scheme. If an access token is expired, Gatana will use the refresh token to get a new
one.
### API Keys
The configuration is a *list of the key names* which the credentials should provide. The resulting credentials is the key-value pairs of this list which is [provided in a separate step](#provide-the-credentials). The values can be used in headers, command arguments and HTTP headers using this expression:
```
{{ credentials.keys.MY_API_KEY }}
```
### OAuth 2.1
For *OAuth 2.1*, you must provide the authorization server and client details. Gatana supports automatic
discovery of the authorization server and dynamic client registration, or manual input.
If the server is a remote server, you may use the **Discover OAuth from remote
MCP URL** button to attempt to discover the OAuth details automatically. If your remote server does not support this. You can also provide the issuer URL and click the **Discover** button next to the Issuer input field to automatically discover the authorization server details.
If the issuer supports dynamic client registration, i.e. there is a **Client registration endpoint** provided,
you can click the **Auto** button next to the Client ID input field, and Gatana will automatically register a OAuth client.
Some issuers have no registration endpoint, but accept client ID metadata documents. Gatana then uses a client ID
that is the URL of a small JSON document that Gatana publishes for the server. The issuer reads the client name
and the redirect URI from that document. Each server has its own document, and the client has no secret, so the
sign-in uses PKCE. The **Auto** button sets up this client too. The issuer must be able to fetch the document, so
your Gatana installation needs a public URL that starts with `https://` and that the issuer can reach.
For almost all cases, the **Authorization Code** grant should be used. Gatana also supports the **Device Code**
grant, note however that some issuers (e.g. Google) places limits the credentials privileges when using this device code grant.
Some providers, e.g. Atlassian, returns ID tokens that has a different issuer (`iss` claim) than the MCP metadata issuer. Gatana rejects these tokens by default. An organization owner can accept them: open **Settings** and turn on **Allow different OAuth issuer paths**.
### OAuth Client Credentials
For *OAuth Client Credentials*, you must provide the only the authorization server metadata in the server settings. Each credential consist of a Client ID + Client Secret and optional scopes, which will be used to generate an access token.
## Credentials
The credentials are the resolved values which will be used when communicating with the server. The credentials can be scoped as following:
* **Server:** there is one (1) set of credentials which will be used by all Gatana users accessing the tools of the server.
* **User:** each Gatana user has their own credentials. A common scenario could be a server for e-mail, where each Gatana user should only get access to their own mailbox.
* **Profile:** if the MCP session has a [profile](/profiles) applied, and if the profile has a credential set for this server
We recommend to use [Profiles](/profiles) if you want a different set of credentials for each of your MCP clients. For example, one set for e.g. Claude Code, one for GitHub Copoilot etc.
### Provide the Credentials
There are three ways of providing credentails, directly in the Gatana App, through MCP protocol, or the Gatana Public API.
#### Gatana App
Navigate to the server details, and click on the *Credentials* setting. On this
page, you can provide your user-scoped credentials, and, if you have administration privileges for
this server, you can also provide the the default credentials.
#### MCP Protocol
The other way of providing credentials is over the MCP protocol; when Gatana calls a server on-behalf
of a Gatana user, it will look up any required credentials. If credentials are missing, Gatana will fail the
request with a MCP response looking like this:
```js
{
errorCode: 'need-credentials',
authorizeUrl: 'https://gatana.ai/api/v1/mcp-servers/oauth/gt_V1St_GX-R8/begin',
message: 'This tool requires secondary authorization. You must tell the end-user to ' +
'please go to the authorizeUrl. After this is done, you can retry the tool call and it will work.',
}
```
The MCP client, i.e. the agent, is at this point expected provide the `authorizeUrl` to the end-user. The
page under the URL will ask the user to fill out the necessary API Key values, or redirect the user to the configured
OAuth 2.1 server to authorize, depending on the authorization method. This is only requried once per user.
The user does not sign in to Gatana during this process, because the token in the URL identifies the user.
For this reason, a link is good for one authorization only, and it must be opened within 24 hours after
Gatana made it. When a link was used or has expired, the agent gets a new one on the next tool call that
needs it.
Gatana sends the same response when the provider no longer accepts a stored OAuth login, for example
because the login was revoked or has expired. The message then says that the login was rejected. For a
shared login, that is default or profile credentials, the message also says that everyone who uses it
will act as the account that signs in.
If the *default credentials* are missing and if user does not have the administration privileges for
this server, then Gatana will not provide a `authorizeUrl`. It will instead inform the MCP client to let
end-user know to contact their Gatana adminstrator. When the provider rejected a shared login, the
message names the person who authorized it, if Gatana knows who that was.
#### Gatana Public API
If you would like to apply your own logic, you can use the Gatana Public API directly. You can [check if user has credentials](/routes/mcp-servers/serverslug/credentials/user/get/), [get the URL where user can authorize over OAuth](/routes/mcp-servers/serverslug/credentials/authorize-url/get/), or [set the credentials directly](/routes/mcp-servers/serverslug/credentials/user/put/).
## Limitations
Only default credentials are availble for used in environment variables and command. When deploying a non-remote server, any expression will expand to the *default* credentials.
Gatana considers this a high-priority issue, and is working on improving the user-scoped credential for local servers
# Email (IMAP & JMAP)
Connect a mailbox so agents can read email, organize messages, create drafts, and send replies
Gatana connects directly to your email provider through IMAP or JMAP and provides MCP tools for the mailbox. You do not need to run a separate email MCP server.
An agent can search messages, read attachments, and prepare replies. You choose whether it can also change the mailbox or send email. Each user connects their own mailbox by default.
For the provider rules behind the presets, and how to choose an access level, see the guide [How to connect email to MCP](https://www.gatana.ai/guides/connect-email-to-mcp-imap-jmap).
## Add an email server
1. Open **Servers** and create a server with the type **Email**.
2. Select a **Provider**. Gatana fills in the connection settings for Gmail, Microsoft 365, Fastmail, and iCloud Mail. For another provider, select **Other (IMAP)** or **Other (JMAP)** and enter its address.
3. Select the **Authorization Method** if the provider offers a choice.
4. Select an **Access Scope** from the table below.
5. Enter a server slug and select **Create Server**.
6. If you use OAuth, complete the provider setup in the server's **Settings** tab.
7. Open **Credentials** and connect your mailbox. Each user does this with their own account. An agent can also provide a connection link when a tool call needs credentials.
See [Credentials](/credentials) for sign-in options and [Profiles](/profiles) to use different mailboxes with different clients.
## Access levels
The access level controls which tools Gatana provides. Your email provider must also allow the requested actions.
| Access Scope | What the agent can do |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Read only** | List folders, search and read messages, and read attachments. Reading a message does not mark it as read. |
| **Read and organize** | All read actions, plus mark messages as read or unread, change flags, move messages, move messages to Trash, and create drafts. A person can review and send the drafts in their mail app. |
| **Read, organize and send** | All read and organize actions, plus send new messages and replies. |
For IMAP, Gatana provides `send_message` only if an SMTP server is configured. For JMAP, the server and the user's token must support sending.
## Provider setup
### Gmail
The Gmail preset uses IMAP to access mail and SMTP to send it. You can use an app password or OAuth.
To use **Email + Password**:
1. Turn on 2-Step Verification for your Google Account.
2. Create a [Google app password](https://support.google.com/mail/answer/185833?hl=en). Account or organization policy can prevent this option from appearing.
3. In Gatana **Credentials**, save your full email address as `username` and the app password as `password`.
To use **OAuth**:
1. Select **OAuth** when you create the server.
2. In Google Cloud, configure the consent screen and create an OAuth client of type **Web application**.
3. Add the redirect URI shown in the Gatana OAuth settings to the client.
4. Save the client ID and secret in Gatana. Keep the preset scopes: `openid`, `email`, and `https://mail.google.com/`.
5. Save the settings. Each user can then connect through **Credentials**.
The mail scope allows access to IMAP and SMTP through OAuth. The Gatana access level controls which tools the agent can use. See [Google's IMAP and SMTP OAuth documentation](https://developers.google.com/workspace/gmail/imap/xoauth2-protocol).
### Microsoft 365
The Microsoft 365 preset uses OAuth with Exchange Online. Your organization must register an app before users can connect.
1. Register an app in Microsoft Entra ID. Add the delegated Exchange Online permissions `IMAP.AccessAsUser.All` and `SMTP.Send`.
2. Add the redirect URI shown in Gatana to the app registration.
3. Save the app's client ID and secret in the Gatana OAuth settings.
4. In the issuer, authorization URL, and token URL, replace `organizations` with your **Directory (tenant) ID**. Keep the preset scopes, including `openid`, `email`, and `offline_access`.
5. Save the settings. Each user can then connect through **Credentials**.
See Microsoft's [IMAP and SMTP OAuth setup](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth/).
To send email, the mailbox must also allow **Authenticated SMTP**. An administrator can check this in the Microsoft 365 admin center under **Users → Active users → Mail → Manage email apps**. Organization policies can also block SMTP AUTH. See [Microsoft's SMTP AUTH settings](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).
### Fastmail
The Fastmail preset uses JMAP with an API token. It fills in the session URL automatically.
1. In Fastmail, open **Settings → Privacy & Security → Manage API tokens**.
2. Create a token with access to mail. Allow sending only if the agent needs it.
3. In Gatana **Credentials**, save the API token as `token`.
4. If the token cannot send, set the Gatana access level to **Read and organize** or **Read only**.
See [Fastmail's API documentation](https://www.fastmail.com/dev/) for token and JMAP details.
### iCloud Mail
The iCloud preset uses IMAP and SMTP with an app-specific password.
1. Sign in at [Apple Account](https://account.apple.com).
2. Open **Sign-In and Security → App-Specific Passwords** and create a password for Gatana. Your Apple Account must have two-factor authentication enabled.
3. In Gatana **Credentials**, save your iCloud email address as `username` and the app-specific password as `password`.
See [Apple's app-specific password instructions](https://support.apple.com/en-us/102654).
## Other providers
### IMAP and SMTP
Select **Other (IMAP)** and enter the IMAP host, such as `imap.example.com`. To allow sending, also enter an SMTP host. Leave it empty to prevent sending.
The creation form uses port `993` with TLS for IMAP and port `465` with TLS for SMTP. After creation, you can change the hosts, ports, and encryption in **Settings**. STARTTLS requires a TLS upgrade before sign-in. Gatana stops if the upgrade fails.
Each user saves `username` and `password` in **Credentials**. SMTP uses the same credentials as IMAP. Use an app password if your provider requires one.
The **Save a copy in Sent** setting saves an extra copy after SMTP accepts a message. Turn it off if your provider already saves sent messages. The Gmail and Microsoft 365 presets have this setting off.
### JMAP
Select **Other (JMAP)** and enter the session URL supplied by your provider, such as `https://mail.example.com/.well-known/jmap`.
Each user saves an API token as `token` in **Credentials**. Gatana sends it as a Bearer token. You can change the headers in **Settings** if your server needs a different format.
Gatana uses the session's primary mail account by default. Set **Account ID** in **Settings** to select another account available in the session. Sending uses JMAP and does not need SMTP.
You cannot change the protocol after you create the server. To switch between IMAP and JMAP, create another server.
## Available tools
| Tool | Minimum access | Action |
| ----------------- | -------------- | ---------------------------------------------------------------------------------------- |
| `list_mailboxes` | Read | List folders, their roles, and message counts. |
| `search_messages` | Read | Find messages in one folder, newest first. Return message summaries and the match count. |
| `get_message` | Read | Read a message's headers and body, and list its attachments. |
| `get_attachment` | Read | Read one attachment. Text files return as text; other files return as base64. |
| `set_flags` | Organize | Mark messages as read or unread, or add or remove flags. |
| `move_messages` | Organize | Move messages to another folder. IMAP message IDs change after a move. |
| `trash_messages` | Organize | Move messages to Trash. They can be restored until the mail server empties Trash. |
| `create_draft` | Organize | Save a plain-text message or reply in Drafts for review. |
| `send_message` | Send | Send a plain-text message or reply from the connected mailbox. |
### Search and read
Search defaults to the inbox. Use `list_mailboxes` to find another folder and pass its ID as `mailbox`. Filters include text, sender, recipient, subject, date, read status, and flag status. The `text` filter supports Gmail search syntax on Gmail.
Search returns up to 20 messages by default. Set `limit` from 1 to 100. Use the returned message IDs with `get_message` to read the bodies.
Message bodies return as plain text by default, with a limit of 20,000 characters. Set `maxLength` from 1,000 to 200,000 to change this limit. The result sets `isTextTruncated` when the text exceeds the limit.
If a link appears only in the HTML body, request `format: "html"`. Gatana converts the HTML to Markdown.
Attachments have a separate size limit. `get_attachment` accepts up to 1,000,000 bytes by default. Set `maxBytes` up to 10,000,000. Larger attachments are rejected.
### Organize and reply
Flag, move, and trash operations accept up to 100 message IDs per call. After an IMAP move, use the returned `newId`, when available, or search again.
To prepare a reply, set `inReplyTo` to the original message ID and provide the body as `text`. If you omit `to`, Gatana uses the original message's reply address or sender. If you omit `subject`, Gatana uses the original subject with a reply prefix. It also sets the reply headers.
Drafts and sent messages support `to`, `cc`, and `bcc`. These tools accept plain-text bodies without outgoing attachments. Use your mail app to send a saved draft. The `send_message` tool creates and sends a new message; it cannot send a saved draft by ID.
# Identity Federation (OIDC/SAML)
Connect Gatana to your organization identity providers
## Introduction
For the full picture, from agent sign-in to SCIM and claim mapping, see the guide [How to add SSO, SAML and SCIM to MCP servers](https://www.gatana.ai/guides/how-to-add-sso-saml-scim-to-mcp-servers).
You can connect Gatana to any **OIDC Core 1.0** and **SAML** compliant identity provider. For a single user's account, Gatana might maintain several identities matched *on the email*. Meaning if a user is created with email `tom.smith@mail.com`, if they then sign-in using federated identity with the same email, the resulting account will be the same.
## Adding an Identity Provider
To add an identity provider, OIDC or SAML, follow these steps in Gatana App:
1. Go to **Settings** by clicking in the left sidebar
2. Scroll down to **Authentication**
3. Here you can add your **OpenID Connect** or **SAML** provider by clicking on **Add**
### OpenID Connect
To see the Redirect URI Gatana uses for your organization, go to the **Settings** and click on **Add**, or **Edit** for your OIDC identity provider configuration. In the dialog that opens, scroll to the bottom. This is the URL pattern:
```
https://YOUR_ORG_ID.gatana.ai/oauth/interaction/auth-callback
```
#### OIDC Access Token Trust
If you want to use access tokens issued by the OIDC issuer to authenticate requests, you can enable this setting. If enabled, Gatana will accept any access token which (1) is issued by the configured federated OIDC identity provider and (2) has the configued **Client ID** as its audience.
If no existing user with a matching `sub` or `email` is found, a new user will be provisioned.
### SAML
To see these SAML values for your organization, go to the **Settings** and click on **Add**, or **Edit** for your SAML identity provider configuration. In the dialog that opens, scroll to the bottom. This are the URL patterns:
**Service Provider Metadata URL**
```
https://YOUR_ORG_ID.gatana.ai/api/v1/tenant-saml-metadata
```
**SAML Audience/Entity ID**
```
https://YOUR_ORG_ID.gatana.ai
```
**ACS URL**
```
https://YOUR_ORG_ID.gatana.ai/oauth/interaction/saml-callback
```
## Multiple Identity Providers
At the moment you can only configure one OIDC and one SAML provider. Gatana does not currently support more than one (1) of each.
# Gatana CLI
Manage your organization from your terminal
The Gatana CLI ([GitHub](https://github.com/gatana-ai/gatana/tree/main/packages/gatana-cli)) written in Rust is a quick and easy to use CLI to manage most aspects of your organization.
```bash
curl -fsSL https://github.com/gatana-ai/gatana/releases/latest/download/install.sh | sh
gatana config login
```
```bash
brew install gatana-ai/tap/gatana
gatana config login
```
You can always self-upgrade:
```bash
gatana upgrade
```
## Examples
### Adding a Remote MCP Tool Server
```ansi
[1m$ gatana create server remote https://mcp.linear.app/mcp[0m
Slug: linear
OAuth found: https://mcp.linear.app
[32mCreated server linear.[0m
[32mRegistered an OAuth client at https://mcp.linear.app.[0m
[1mNext steps:[0m
1. Sign in to the service: gatana create creds linear
2. Choose who can use it: https://acme.gatana.ai/dashboard/servers/linear
[1m$ gatana create creds linear[0m
[32mOpen this URL to authorize. You can also provide OAuth tokens as JSON with -f or stdin:[0m
[32mhttps://gatana.ai/api/v1/mcp-servers/oauth/gt_i41yw2b2c4/begin[0m
[1m$ gatana tools ls --server linear[0m
[1mNAME[0m [1mDESCRIPTION[0m
linear_create_attachment Create a new attachment on a specific Linear issue by uploading base64-encoded…
linear_create_issue_label Create a new Linear issue label
(...)
```
### Inspect Tools
```ansi
[1m$ gatana tools fetch_fetch_markdown[0m
[1mfetch_fetch_markdown[0m
Server: fetch
Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.
[1mArguments:[0m
[1mNAME[0m [1mTYPE[0m [1mREQUIRED[0m [1mDESCRIPTION[0m
url string yes
headers string[] no Optional headers to include in the request, as an array of strings like ["Heade…
[1mExample:[0m
gatana tools call fetch_fetch_markdown -a '{"url":"...","headers":["..."]}'
```
### List servers
```ansi
$ gatana get servers
[1mSLUG[0m [1mENABLED[0m [1mTYPE[0m [1mLAST UPDATED[0m [1mAGE[0m
skills [32mtrue[0m self 13h52m 13h52m
artifacts [32mtrue[0m self 13h52m 13h52m
codemode [32mtrue[0m self 13h52m 13h52m
compression [32mtrue[0m self 13h52m 13h52m
fetch [32mtrue[0m self 13h52m 13h52m
```
### Execute Tool
```ansi
$ gatana tools call gmail_list_labels
{
"labels": [
{
"labelId": "CHAT",
"name": "CHAT",
"threadsTotal": 0,
"threadsUnread": 0,
"messagesTotal": 0,
"messagesUnread": 0,
"labelType": "SYSTEM",
"messageListVisibility": "HIDE",
"labelListVisibility": "LABEL_HIDE"
},
(...)
]
}
```
Code mode allows an agent to submit NodeJS code that is executed by Gatana in an isolated microVM. [Documentation](/code-mode/).
```ansi
$ gatana tools call codemode_execute_code -a code='const { labels } = await codemode.gmail.listLabels(); return labels[0]'
{
"labelId": "CHAT",
"labelListVisibility": "LABEL_HIDE",
"labelType": "SYSTEM",
"messageListVisibility": "HIDE",
"messagesTotal": 0,
"messagesUnread": 0,
"name": "CHAT",
"threadsTotal": 0,
"threadsUnread": 0
}
```
### Permissions
```ansi
[1m$ gatana create team platform --description "Platform engineers" -a ada@example.com -a bob@example.com[0m
[32mCreated team 'platform' with ada@example.com, bob@example.com.[0m
Give it access: gatana server grant --team 'platform', or gatana profile assign --team 'platform'
[1m$ gatana server grant github --team platform --role maintainer[0m
[32mGave team 'platform' the role maintainer on server 'github'.[0m
[1m$ gatana server access github[0m
[1mKIND[0m [1mNAME[0m [1mROLE[0m
account ada@example.com admin
team platform maintainer
profile Engineering member
```
### Service accounts and personal access tokens (PATs)
```ansi
[1m$ gatana create service-account "CI runner"[0m
[32mCreated service account 'CI runner' (ci-runner@service-account.acme.gatana).[0m
API key: gk_1_…
[1m$ gatana create pat --account ci-runner@service-account.acme.gatana --name deploy --profile Engineering[0m
[32mCreated token 'deploy' for 'ci-runner@service-account.acme.gatana'.[0m
API key: gk_1_…
```
### Profiles
```ansi
[1m$ gatana create profile Engineering --description "Tools for engineers" -s github -s linear[0m
[32mCreated profile 'Engineering' with github, linear.[0m
Give it to someone: gatana profile assign 'Engineering' --account | --team
[1m$ gatana profile assign Engineering --team platform[0m
[32mGave profile 'Engineering' to team 'platform'.[0m
[1m$ gatana get profiles[0m
[1mNAME[0m [1mOPEN TO ALL[0m [1mRESTRICTIVE[0m [1mCODE MODE[0m [1mSERVERS[0m [1mUSERS[0m [1mTEAMS[0m [1mAGE[0m
Engineering false false false 2 0 1 12s
```
### Organization Settings
```ansi
[1m$ gatana patch org -p allowMemberAddLocalServers=false[0m
Patch from key=value pairs: {"allowMemberAddLocalServers":false}
[32mUpdated the organization: allowMemberAddLocalServers.[0m
[1m$ gatana patch org --logo ./logo.png[0m
[32mUpdated the organization: logo.[0m
```
# Gatana for your organization
Set up Gatana for everyone in your organization
You can connect your own account by following [Connecting to Gatana](/connecting-to-gatana). To set up Gatana for your whole team, you can use your organization's admin settings. This can make installing for everyone a lot easier.
This page explains how admins can make Gatana available in ChatGPT, Codex, and Claude.
**Supported environments**
* ChatGPT (Work, Chat)
* Codex
* Claude Code
* Claude.ai
* Claude Cowork
## Claude Code
**MCP connector**
1. Open [Claude Code Admin Settings](https://claude.ai/admin-settings/claude-code).
2. Add the following configuration to **Managed settings**. Replace `YOUR_ORG_ID` with your Gatana organization ID:
```json
{
"managedMcpServers":{
"gatana": {
"type": "http",
"url": "https://YOUR_ORG_ID.gatana.ai/mcp"
}
}
}
```
For more information, see the [Claude managed settings documentation](https://code.claude.com/docs/en/managed-mcp#provide-servers-through-managed-settings).
**Skills**
1. Open [Skills settings](https://claude.ai/admin-settings/skills).
2. Add the [Gatana skill](https://github.com/gatana-ai/gatana-plugins/blob/main/plugins/claude/skills/gatana-skills/SKILL.md).
3. In the skills list, open the menu on the right of the Gatana skill and select **Default Access: Required**.
For a more reliable setup, use Claude Code's `SessionStart` hooks instead of the Gatana skill. Each member can run `gatana install` to set up the hooks. See [Connecting to Gatana](/connecting-to-gatana/) for details.
## Codex
1. Open your workspace's [Codex policies](https://chatgpt.com/codex/settings/managed-configs) under **Admin Console > Agent Security**.
2. Add Gatana to the managed defaults (`config.toml`). Replace `YOUR_ORG_ID` with your Gatana organization ID:
```toml
[mcp_servers.gatana]
url = "https://YOUR_ORG_ID.gatana.ai/mcp"
```
3. Ask each member to run `codex mcp login gatana` once and sign in with their Gatana account.
For more information, see the [Codex managed configuration documentation](https://learn.chatgpt.com/docs/enterprise/managed-configuration).
## Claude.ai & Claude Cowork
This setup is available for organization accounts only.
1. Open [Skills settings](https://claude.ai/admin-settings/skills).
2. Select **Add > Upload a Plugin**.
3. Download the [Gatana plugin ZIP file](https://github.com/gatana-ai/gatana-plugins/releases/download/v1.0.1/gatana-chatgpt-plugin-v1.0.1.zip) and upload it.
4. Select **Add** to add the Gatana connector.
5. In the plugins list, open the menu on the right of the Gatana plugin and select **Default Access: Required**.
This installs a basic Gatana skill and the Gatana connector for each member. Each member must then connect Gatana with their own account.
## ChatGPT Work & Chat
This setup is available for Business and Enterprise workspaces.
1. Open the [Admin Console](https://chatgpt.com/admin), select your workspace, and open **Plugins**.
2. Select **Add > Import marketplace**.
3. Enter `https://github.com/gatana-ai/gatana-plugins` as the **Source**. Leave **Path** empty because the repository contains `.agents/plugins/marketplace.json` at its root. Leave **Branch** empty to use the default branch.
4. Select **Import marketplace** and authorize GitHub.
5. Review the import results and open the **Gatana** plugin. Set the **Installation policy** to **Installed** to install it automatically for members. Choose **Available** if members should install it themselves.
Business workspaces use workspace-wide availability. If your workspace supports role controls, configure access for each intended role. Make sure members can access any apps required by the plugin.
ChatGPT checks the repository for updates once a day. To get updates immediately, select **Sync now** under **Plugins > Marketplaces**.
ChatGPT marks the plugin as **Desktop only** because it includes an MCP server. Members can use it in Chat, Work, and Codex in the ChatGPT desktop app. Codex CLI installation is separate.
After installation, each member must:
1. Connect Gatana and sign in with their Gatana account when prompted.
2. Start a new chat to use the plugin.
For more information, see [ChatGPT plugin management](https://learn.chatgpt.com/docs/enterprise/plugin-management), [plugin controls](https://learn.chatgpt.com/docs/enterprise/apps-and-connectors), and [plugin setup](https://learn.chatgpt.com/docs/plugins).
# Introduction
Get started with Gatana Gateway
The Gatana Gateway is a collection of MCP Servers. Each server provides a set of tools. When you add a server to your gateway, its tools will become available to the Gatana users.
* See [Model Context Protocol Official Website](https://modelcontextprotocol.io/docs/getting-started/intro) for more information.
* New to the idea of a gateway? The guide [What is an MCP gateway?](https://www.gatana.ai/guides/what-is-an-mcp-gateway) explains what it does, when a team needs one and what it costs in latency and tokens.
Each server in Gatana has a **Slug**. This is a technical identifier (a-z and hyphens only). Keep it short; it will be
prepended to the tool names which commonly have a limit of 64 characters. This is done to keep each tool name unique.
Gatana formats the tool name so: `${slug}_${toolname}`.
The tools of each server are cached and are automatically refreshed periodically every couple of hours. To refresh the tool cache manually, you can navigate to the server in Gatana App and click on the **Refesh** button under the Tools section. Tools
## Adding a Server
In the header right click on **Add Server**. You have several choices:
* **Popular servers**: a catalog of well-known services (GitHub, Linear, Notion, Slack, Google
Workspace, and more). One click installs the vendor's official server and, where the vendor
allows it, sets up the OAuth sign-in automatically. A few vendors, such as Google Workspace,
require an OAuth client of your own; the server's settings show what is left to fill in.
* **By transport**: configure any server yourself. The choices are grouped by where the server
runs; see [Server Types](#server-types) below.
* **Search MCP registries**: search the official MCP registry by name, and the [Glama](https://glama.ai) directory.
* **Built-in**: tools Gatana provides itself, with nothing to host.
You can also ask the [AI assistant](/ai-assistant) to set a server up for you.
## Server Types
Gatana supports three different categories of servers:
* **[Standard MCP](/servers-standard-mcp)** (remote HTTP servers and executable apps)
* **[FaaS-style Runnable Code](/servers-runnable-code/)**
* **[Public OpenAPI / Swagger endpoints](/servers-openapi-swagger)**
This only affects how Gatana talks to one particular server. Its tools and capabilities will always be available through the Gatana Gateway using the remote MCP protocol.
## Access
Gatana supports flexible access configurations. A server can grant access based on individual users or
teams. The following roles are available:
* **Member:** able to see and call the tools
* **Maintainer:** able to modify the server configuration
* **Admin:** modify permissions and delete the server
Each user's base permissions is controlled by the organization-wide **Base Permissions** setting.
It provides least privileges each user has organization-wise. To only grant access on a per-server basis
the "Base Permissions" should be set to "No permissions".
See [Permissions and Roles](/permissions-and-roles) for more information.
# Tools over HTTP + OpenAPI
Access all your tools over HTTP, well-specified using OpenAPI
Sometimes you are working in environments where MCP client are not the best fit. To help in these situations, Gatana offers a simple HTTP API to access all your tools. The URL to access the OpenAPI specification is: `https://YOUR_ORG.gatana.ai/api/mcp-httpapi/openapi.json`
The authorization requirements stay the same as described in [Connecting to Gatana](/connecting-to-gatana#how-the-sign-in-works).
## Long-running calls
If you have a long-running tool, that might trigger timeouts due to no data-transfers, you can set `Accept` header to `text/event-stream`. Gatana will send a `:keepalive` every 15 seconds, and finally close the stream with a `:data` event containing the JSON serialized result.
## Example
```bash
$ curl https://ORG_ID.gatana.ai/api/mcp-httpapi/openapi.json -H"Authorization: Bearer $TOKEN" | jq
(...)
"/api/mcp-httpapi/tools/github_pull_request_read": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"method",
"owner",
"repo",
"pullNumber"
],
"properties": {
"page": {
"type": "number",
"minimum": 1,
"description": "Page number for pagination (min 1)"
},
"repo": {
"type": "string",
"description": "Repository name"
},
"after": {
"type": "string",
"description": "Cursor for pagination, used only by the get_review_comments method. Pass the endCursor from the previous page's PageInfo to fetch the next page."
},
"owner": {
"type": "string",
"description": "Repository owner"
},
"method": {
"enum": [
"get",
"get_diff",
"get_status",
"get_files",
"get_commits",
"get_review_comments",
"get_reviews",
"get_comments",
"get_check_runs"
],
"type": "string",
"description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get combined commit status of a head commit in a pull request.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_commits - Get the list of commits on a pull request. Use with pagination parameters to control the number of results returned.\n 6. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.\n 7. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned.\n 8. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n 9. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.\n"
},
"perPage": {
"type": "number",
"maximum": 100,
"minimum": 1,
"description": "Results per page for pagination (min 1, max 100)"
},
"pullNumber": {
"type": "number",
"description": "Pull request number"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {}
}
}
}
}
},
(...)
```
You can then call the tool like so:
```bash
curl https://ORG_ID.gatana.ai/api/mcp-httpapi/github_pull_request_read \
-X POST
-d method=get \
-d owner=gatana-ai \
-d repo=gatana-js \
-d pullNumber=1 -XPOST \
-H"Authorization: Bearer $TOKEN" | jq
{
"result": "{\"number\":1,\"title\":\"feat: ma(......)merged_at\":\"2026-02-28T10:32:37Z\"}",
"isError": false
}
```
# Import and Export
Download servers as a JSON file, and restore them in the same or another organization
## Introduction
A server's configuration can be downloaded as a JSON file, and a JSON file can be imported to create servers. Use it to back up a server, copy servers between organizations (for example from a test organization to production), or move to a [self-hosted installation](/on-premise-self-hosting).
All server types can be exported and imported except **built-in** servers, which every organization already has.
## Exporting
There are two ways to export:
* **One server:** open the server's **Settings** tab and find **Export as JSON**.
* **Several servers:** on the servers list, select the rows to export, then choose **Bulk actions** and **Export JSON**.
Both open the same dialog, where you choose what goes into the file:
| Option | What it adds |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Include sensitive fields** | OAuth client secrets, Tailscale auth keys, and the values of environment variables and headers. When off, the names are kept and the values are blanked. |
| **Include server-scoped credentials** | The [credentials](/credentials) stored on the server itself. Credentials owned by profiles or user accounts are never exported. |
| **Include source code** | The code package of [Runnable Code](/servers-runnable-code) servers. Shown only when the export contains one. |
Values like `{{ credentials.keys.MY_API_KEY }}` are always kept. They reference the credential store and are not secrets themselves.
With the first two options on (the default), the file holds API keys, tokens and client secrets readably. Treat the file as a secret: store it safely and delete it when it has served its purpose.
The file holds configuration only. Deployment state, logs, and tool lists are not exported; the importing organization discovers tools on its own when the server deploys.
## Importing
Open **Add Server** and choose **Import from JSON**, or use the **Import** button on the servers list. Pick the file, and the page lists every server in it for review before anything is created.
When a server in the file has the same slug as an existing server, you choose per server:
* **Add** it under a new slug (a numeric suffix such as `-2` is proposed), or
* **Replace** the existing server with the imported configuration.
Importing creates the servers, stores their server-scoped credentials, and uploads their source code. A server that is enabled in the file deploys right away. Servers that fail to import do not stop the rest; the result is reported per server.
After an import you may still have work to do:
* If sensitive fields were excluded from the export, the imported servers have their environment variable, header and secret values blank. Fill them in under the server's settings.
* Credentials owned by profiles or user accounts are not in the file. Users connect their own credentials again, as described on the [credentials page](/credentials).
## Permissions
Exporting a server requires permission to change it, and exporting its stored credentials requires permission to read their secrets. Importing requires permission to create servers, and replacing an existing server requires permission to change that server. See [Permissions and Roles](/permissions-and-roles).
# Introduction
Gatana is a platform with built-in support for MCP, skills, artifacts, SSO, and more. Pick a product to get started, or read the quick start and connect your first agent.
# Limiting Servers
Control what servers are available to your client
To restrict the servers available in your gateway, you can specify the slugs via a query-string parameter in your MCP URL: `https://gatana.ai/mcp?includeOnlyServerSlugs=github,google`
For more sophisticated control, see [Profiles](/profiles).
# On-Premise & Self-Hosting
Instead of using Gatana Cloud, you can run the entire Gatana stack yourself
Self-hosting is available as part of the **Enterprise plan**. Please reach out to your account manager or send us an email at [hello@gatana.ai](mailto:hello@gatana.ai)
## Helm Chart
Helm Chart soruce is available at [https://github.com/gatana-ai/gatana-helm](https://github.com/gatana-ai/gatana-helm)
```bash
helm install gatana oci://ghcr.io/gatana-ai/charts/gatana \
--namespace gatana \
-f values.yaml
```
## Why Self-Host?
Where Gatana Cloud stores and processes data, which sub-processors touch it and what crosses the EU border is set out in the guide [An MCP gateway with EU data residency](https://www.gatana.ai/guides/mcp-gateway-eu-data-residency). Self-hosting is the option for organizations whose rules go further than that.
Self-hosting Gatana gives you complete control over the Gatana infrastructure. Gatana (the company) will never see any of your configuration or data. This is ideal for organizations with strict compliance requirements, data residency obligations, or those who prefer to keep all AI tool interactions within their own network.
### Benefits
* **Data sovereignty** — All data stays within your infrastructure, never leaving your network
* **Compliance** — Meet regulatory and compliance requirements by controlling where data is processed and stored
* **Network isolation** — Run MCP servers that access internal systems without exposing them to the internet
### Trade-offs
* **No automatic updates** — New features and security patches require manual upgrades
* **Infrastructure costs** — You provision and pay for the compute, database, and networking resources
## Configuration
The chart's `values.yaml` documents every setting. The choices that shape an install:
* **Object storage.** Gatana needs two buckets, for uploaded files and for Runnable Code packages. `storage.provider` selects `gcs` (Google Cloud Storage, the default) or `s3`, which covers AWS S3 and S3-compatible stores such as MinIO or Ceph. For S3, set `storage.s3.region`, point `storage.s3.endpoint` at a compatible store (empty means AWS), and either name a credentials secret or rely on ambient credentials (IRSA, node role).
* **Encryption of stored secrets.** `kms.provider` chooses what wraps the organization data keys: Google Cloud KMS, AWS KMS, or nothing. The default is `none`, which stores sensitive values protected only by disk encryption, so review this before production use. See [Confidentiality](/confidentiality#self-hosted-installs) for the table of values.
* **Persistent storage for servers.** [Persistent storage](/persistent-storage) is offered only when `workloads.storageClassName` names a Kubernetes storage class fit for untrusted workloads. `workloads.storageSize` sets the size every volume gets. Left empty, the option stays hidden in the app.
* **Tailscale.** [Tailscale egress](/tailscale) is offered only when `workloads.tailscaleImage` names the sidecar image, on a cluster whose admission policy lets a container hold `NET_ADMIN`. Left empty, the option stays hidden in the app.
* **Audit log retention.** `auditLogs.maxAgeDays` sets how long to keep [audit records](/audit-logs), including credential and sandbox logs. The default is `180`. Use `0` to keep all records, or a whole number of at least 30 days. Invalid values prevent startup. Cleanup runs from 02:00 to 04:00 UTC, with a 5 GiB limit on stored record details per run, checked after each batch. Large backlogs can take several nights. Super-administrators can change retention in **Global Settings**.
* **Glama registry search.** The Add Server registry search also shows remote servers listed on [Glama](https://glama.ai) when `glama.apiKey` (or `glama.apiKeySecret`) provides a Glama API key. Left empty, only the official MCP registry is searched.
* **Email sender.** `email.from` sets the sender for SMTP and SES, for example `Gatana `. Left empty, the sender is `Gatana >`. With SES, AWS must accept the address. Without the chart, set `SENDER_EMAIL`. The older settings `SMTP_FROM` and `SES_FROM` still work and override `SENDER_EMAIL`.
* **OpenTelemetry tracing.** The backend emits traces when the standard `OTEL_*` variables (set through `backend.extraEnv`) point at an OTLP endpoint, and tracing stays off otherwise. Spans cover each MCP request, the connection to the upstream server, and the tool call itself. The trace context travels to upstream MCP servers in the `_meta` field of the call (`traceparent`, `tracestate`, `baggage`), so spans from your own servers can join the same trace.
## Single-tenant mode
In single-tenant mode, an installation serves one organization on its public URL, for example `http://localhost:8080` or `https://gatana.example.com`. It does not need a wildcard DNS record or a wildcard certificate. The setting is `SINGLE_TENANT_MODE`. With Helm, set it through `backend.extraEnv`.
| `SINGLE_TENANT_MODE` | Result |
| ------------------------------------------------------------ | ----------------------------------------------------------- |
| Not set, and the public host is `localhost` or an IP address | On, with the organization ID `app` |
| Not set, and the public host is a host name | Off: each organization is served on `.` |
| An organization ID, for example `acme` | On, with that organization ID |
| `false` | Off |
* The organization ID can have lowercase letters, digits and hyphens, and at most 40 characters.
* An IP address as the public host requires single-tenant mode.
* With an IP host and email enabled, set `SENDER_EMAIL`, for example `Gatana `. An IP address cannot be an email domain.
* With `EMAIL_PROVIDER` unset, copy the invitation link and give it to the user to set a password. The link is shown once and expires after 30 days. Password setup and reset links also go to the backend log. This includes links requested through "Forgot your password?" on the sign-in page. A new owner has no password. The dashboard asks them to set one in My Account.
* Behind a reverse proxy that serves a plain HTTP public URL, set `TRUST_PROXY=true`. Without it, rate limits and audit records use the proxy's IP address for every visitor.
* Set `SINGLE_TENANT_MODE=false` to keep existing `.localhost` URLs.
* On a new install, the first visit opens the setup guide at `/setup`. Whoever opens it first creates the organization and becomes its owner. No other organization can be created.
* All host names select the configured organization. Sign-in sends the browser to the public URL.
* MCP clients connect to `/mcp`.
* The CLI needs the organization ID: `gatana config login app --base-url http://localhost:8080`.
If you enable or disable this mode on an existing installation:
1. Sign in again and reconnect MCP clients. The OAuth issuer changes, for example from `https://acme.gatana.example.com` to `https://gatana.example.com`.
2. Replace set-password links sent before the change. These links stop working.
3. Update the redirect URIs at your SAML or OIDC identity provider. The SSO settings page shows the new URIs.
4. Reload the app in open browser tabs to load the new server settings. The browser can use cached settings for up to one minute after the change.
Other organizations in the database are unavailable in single-tenant mode. The backend logs up to five of their IDs at startup.
## Code Mode without Kubernetes
[Code Mode](/code-mode) runs agent code in a worker. With the Helm chart, Gatana starts a worker pod for each organization. Without Kubernetes, run a separate worker container. Add these settings to the Gatana container:
| Setting | Value |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CODE_MODE_WORKER_URL` | Worker URL that Gatana can reach, for example `http://gatana-worker:8080` |
| `CODE_MODE_CALLBACK_URL` | Gatana URL that the worker can reach, for example `http://gatana:3000`. Set it if the worker cannot reach the public URL. An `--internal` network has no route to the public URL. In a worker container, `localhost` points to that container. |
For example, with Docker:
```bash
# Keep the worker on a network shared only with Gatana
docker network create --internal gatana-worker
docker run -d --name gatana-worker --network gatana-worker \
--read-only --cap-drop ALL --security-opt no-new-privileges --memory 256m \
ghcr.io/gatana-ai/gatana-worker:
# Start the Gatana container as usual, with these two settings added:
# -e CODE_MODE_WORKER_URL=http://gatana-worker:8080
# -e CODE_MODE_CALLBACK_URL=http://gatana:3000
docker network connect gatana-worker gatana
```
* At startup, the backend logs the required worker version. Use that version of `ghcr.io/gatana-ai/gatana-worker`.
* Do not publish the worker port. The worker runs all code that it receives.
* All organizations in the installation share this worker. With the Helm chart, each organization has its own worker pod.
* `CODE_MODE_CALLBACK_URL` requires single-tenant mode. Its host does not identify an organization. With several organizations, leave it unset. The worker calls each organization's public URL. Give the worker a route to these URLs; do not use an internal network in this case.
* If a Kubernetes cluster is available, `CODE_MODE_WORKER_URL` takes priority over worker pods.
* The setup guide shows Code Mode as found when `CODE_MODE_WORKER_URL` is set. If the worker does not respond, `execute_code` returns an error that names this setting.
## Database migrations and upgrades
The backend applies database migrations at startup. A new Kubernetes release starts as follows:
1. The new pod opens its port.
2. The *readiness* check and all other requests return `503`.
3. The *liveness* check returns `200`. The pod is running. Kubernetes waits for readiness before it sends traffic.
4. The pod acquires a lock in Postgres.
5. The pod applies each pending migration and records it in the `migrations` table.
6. After the migrations, the *readiness* check returns `200` and the pod receives traffic.
During a rolling upgrade, old versions continue to serve requests while the new version starts.
### Multiple replicas
You can deploy multiple replicas at the same time. One replica holds the migration lock. The others wait for it to finish.
### Helm upgrades
An index on a large table, such as the audit log, can take many minutes to build.
1. Allow enough time for the migration when you run `helm upgrade --wait` or use `--atomic`. For example, set `--timeout 30m`. If the pod stops during a migration, the next start retries migrations that have no record in the `migrations` table.
2. Keep the chart's liveness probe. It reports the pod as alive throughout the migration. Do not replace it with a probe that fails after a fixed time.
### Setting values
The backend checks settings that have a fixed list of values. An invalid value, such as `EMAIL_PROVIDER=smpt`, stops startup. Some older values still work:
| Setting | Older value | Read as |
| ---------------- | ------------ | ---------- |
| `EMAIL_PROVIDER` | `none` | `disabled` |
| `ENVIRONMENT` | `production` | `prod` |
On a self-hosted install, the backend accepts an unknown `ENVIRONMENT` value with a warning and uses `prod`.
### Database requirements
| Setting | Requirement |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Schema | The default is `public`. Set `POSTGRES_SCHEMA` through `backend.extraEnv`. |
| Permissions | The database user needs `CREATE` on the database to create the schema and the `pgcrypto` and `pg_trgm` extensions. |
| Semantic search (optional) | A superuser must create the `vector` extension. Gatana can run without it. |
Set `DATABASE_MIGRATIONS_AT_START=false` to disable migrations on startup.
### Gatana behind a proxy
Configure the proxy to forward `X-Forwarded-Host` to the Gatana backend. These proxies do this by default:
* Caddy
* Traefik
For nginx, add these settings:
```nginx
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
```
### Interested?
We would be happy to work with you, you can reach us at [hello@gatana.ai](mailto:hello@gatana.ai).
# People & Teams
Organize people into teams to manage groups of users and their permissions
## Introduction
There are three principals in Gatana for which permissions and privileges matter: users, service-accounts and teams. A user represents a human who can sign-in into the Gatana App, service-account is similar to a user but cannot sign-in into the App and does not have identity attached (e.g. federate identity, or a password). A team is a collection of users and service-accounts who inherit the premissions of the team.
## Inviting Users
When you create a user under **People & Accounts**, Gatana sends them an invitation email with a
link to set a password and sign in. The link is valid for 30 days. Until the person sets a
password, the user list marks them as invited.
A user who signs in through an external identity provider does not need the invitation link: see
[Federation](/federation) and [SCIM](/scim-auto-provisioning) for how those users are created.
Service accounts receive no email, because they cannot sign in.
## Service Accounts
Service accounts are useful if you need to connect automation or services with Gatana where either OAuth login flow is not supported, or you would like to limit the servers available. Except that they cannot sign-in to Gatana App, they behave and function just like users.
Note that any organization owner is able to manage the personal access tokens of all service accounts.
## Teams
A team is a collection of users or service accounts which can be granted server roles like a user. [Artifacts](/artifacts) and [skills](/skills) can also be shared with a team. Each member in a team can have one of two roles:
* **Member**: Has no administrative permissions on the team.
* **Maintainer**: Can add and remove team members.
For any action a member does for which they have been granted permissions *through a team*, the audit log will record the *member*, not the team.
### Team Claim Mappings
You can automatically add users to teams based on claims from your external identity provider. When a user authenticates, Gatana checks their claims against configured mappings and adds them to matching teams.
To configure claim mappings, go to the profile detail view and add mappings with a claim key and value.
### Examples
Consider the following claims from your IdP:
```json
{
department: "engineering",
groups: ["engineering", "platform-team"],
branch: {
office: "london"
}
}
```
There are different ways of matching:
| Claim Key | Claim Value | Description |
| --------------- | ------------- | ------------------------------------------------------ |
| `department` | `engineering` | Direct property match |
| `groups` | `engineering` | Array claim - matches if "engineering" is in the array |
| `branch.office` | `london` | Nested property using dot notation |
# Permissions & Roles
Assign granular roles to control access to different servers
## Introduction
You can give organization users, service accounts, and teams different levels of access to servers by assigning them to roles. Choose the role that best fits each member or team's function in your organization without giving people more access to the server than they need. For what those three principals are, and how a team grants a role to its members, see [People & Teams](/people-and-teams).
From least access to most access, the roles for an organization's servers are:
* **No Permissions**: user cannot see or interact with the server
* **Member:** able to see and call the tools
* **Maintainer:** able to modify the server configuration
* **Admin:** modify permissions and delete the server
Organization owners can set base permissions that apply to all members of an organization when accessing any of the organization's servers. For more information, see [How Access Is Decided](#how-access-is-decided) and [Set Base Permissions](#set-base-permissions) below.
## How Access Is Decided
A member can reach a server for any one of four reasons. An empty member list on a server does not mean nobody can use it.
1. **A membership on the server.** The member, or a team they belong to, is added to that server with a role.
2. **A profile.** A [profile](/profiles) that includes the server is assigned to the member or to one of their teams.
3. **The server's Visibility.** A server set to **Organization** can be used, as **Member**, by everyone in the organization.
4. **The base role of the organization.** If it is anything other than **No Permissions**, every member holds that role on every server, without being added to anything. See [Set Base Permissions](#set-base-permissions) below.
A member gets the *highest* role that any of these reasons gives them. Organization owners have full permissions whatever the four say, with two exceptions. A session that applies a [restrictive profile](/limiting-servers) is narrowed to that profile, owner or not. And an organization can decide that its owners are offered tools over MCP the way members are; see [Owners Over MCP](#owners-over-mcp) below.
### Visibility Is Not The Same As Access
A server's **Visibility** and the organization's base role are two independent ways in. Either one on its own is enough to let every member use a server:
| Base role | Visibility | Who can use the server |
| --------------------------- | ------------ | ------------------------------------------------------------- |
| No Permissions | Private | Only the people and teams you added, plus profiles and owners |
| No Permissions | Organization | Every member, as **Member** |
| Member, Maintainer or Admin | Private | Every member, at the base role |
| Member, Maintainer or Admin | Organization | Every member, at the base role |
Set the server's Visibility to **Private** *and* the organization's base role to **No Permissions**. Changing only one of the two leaves the server open to every member.
For narrowing what one client or agent may reach, use a [restrictive profile](/limiting-servers) instead.
## Base Member Privileges
You can set base permissions that apply to all members of an organization when accessing any of the organization's servers.
To change it, go to **Settings** in the left sidebar and find **Base Member Privileges**. In the public API the field is `memberDefaultRole` on the organization.
**A new organization starts at No Permissions.** Members then reach a server only through a membership, a profile, or the server's Visibility. Raise the base role when you want everyone to have the same role everywhere without granting it server by server.
If someone with admin access to an organization's server grants a member or a team a higher level of access for the server, the higher level of access overrides the base permission. The base role never lowers a role somebody already has.
Members with the organization role of Owner always have full permissions, whatever the base role is.
Note that all changes to base permissions will affect both new and existing members.
The base role applies to every server in the organization, including servers added later. Setting it to **Admin**, for example, lets any member change the configuration of, and delete, every server.
## Organization Owners & MCP
Gatana will by default only send the tools to an organization owner if they are granted access to them as non-owner would. This is since an owner can see and manage everything in the organization, and they might use Gatana over MCP but not need everything (for example if they configured Gatana for several different teams).
This is configurable under **Settings** in the left sidebar, with **Make all tools available to organization owners over MCP**:
* **Off (default)**: an owner over MCP is sent only the tools they have explicit grants to (like a non-owner)
* **On**: all the tools of every server in the organization are sent.
## Credentials and Clients
You restrict a credential, client (e.g. OAuth client), or a team, by using a [profile](/profiles).
## Server Creation Privileges
In a new organization by default all users are allowed to create servers. You can control this in the organizational settings.
**Whoever creates a server becomes its Admin.** They can configure it, decide who else reaches it, and delete it, without anybody granting them anything. This matters because a new server is Private and the base role is No Permissions, so without it the creator could not see their own server.
# Storage
Keep a server's files across restarts and redeploys with a persistent data volume
## Introduction
Servers that Gatana deploys for you ([Executable Apps](/servers-standard-mcp#executable-apps) and [Runnable Code](/servers-runnable-code)) start from a fresh filesystem every time they deploy. Whatever the server writes to disk is gone after a restart, a redeploy, or the automatic stop of an idle server.
With persistent storage the server keeps one data volume, mounted at `/data`. Files under that path survive restarts, redeploys, and idle stops. Use it for state worth keeping between restarts: a SQLite database, a cache that is expensive to rebuild, or files an agent accumulates over time.
Remote HTTP and OpenAPI servers run outside Gatana, so this setting does not exist for them.
## Turning It On
Check **Attach Persistent Storage** when creating the server, or enable it later under **Storage** in the server's **Settings** tab. The server restarts to mount the volume.
The mount path is also in the environment variable `GATANA_DATA_DIR`, so your code can read the variable instead of hardcoding `/data`.
Every volume has the same fixed size, set by the platform. The server's **Deployment** tab shows the size and how much of it is used, and the deployment metrics chart includes the usage over time.
If the option is not offered, persistent storage is not available in your environment. In the Gatana cloud service it is part of the paid plans. A [self-hosted installation](/on-premise-self-hosting) offers it only when it is [configured with a Kubernetes storage class](/on-premise-self-hosting#configuration) for the volumes.
## The Organization Quota
An organization can claim a fixed total of storage across all its servers: 20 Gi, unless a different quota was agreed for the organization. The quota counts the size the volumes claim, not the bytes written to them. Turning storage on for a server is refused when it would push the total past the quota. Contact [support@gatana.ai](mailto:support@gatana.ai) to raise the quota.
## Lifecycle
A volume is always in one of four states, and each action moves it to the next one. Turning storage off is not the same as deleting the data: only steps 5 and 6 destroy it.
| State | Where the data is | Quota | The state ends when |
| ------------ | ---------------------------------------------------------------- | ------- | --------------------------------------------------- |
| **Attached** | On the volume, mounted at `/data` | Counted | You turn storage off, or you delete the server |
| **Detached** | Kept on the volume | Counted | You turn storage on again, or you delete the server |
| **Orphaned** | Kept on the volume of the deleted server | Counted | 50 days pass, or an owner deletes the volume |
| **Deleted** | Gone. Volumes are not backed up, so the data cannot be recovered | Freed | Never. This state is the end |
Points to know about each step:
* **Turn storage off** (in the server settings, or **Detach** on the Storage page). The server restarts without the volume. The volume and its data are kept.
* **Turn it on again** and the same volume is mounted, with the files as they were. If the volume was deleted in the meantime, the server gets a new, empty one.
* **Delete the server** and its volume becomes orphaned. The Storage page lists it until it is deleted. An orphaned volume stays with the deleted server and cannot be attached to a different one, so a new server with the same name starts with empty storage.
* **Delete the volume** on the Storage page and the data is destroyed immediately. When the owning server still has storage turned on, this also turns storage off for that server.
## The Storage Page
Organization owners can manage all volumes under **Storage** in the left sidebar. The page lists every volume in the organization, including the orphaned volumes of deleted servers, shows the total claimed against the quota, and charts the usage of each volume over time. Volumes can be detached, attached again, and deleted here.
Servers connected to [Tailscale](/tailscale) also appear here with a small volume holding their machine identity. Identity volumes do not count against the quota, cannot be detached or attached, and can only be deleted once the server no longer uses them.
## Redeploy Behavior
A volume can be attached to only one running instance at a time. A server with storage therefore stops its old instance before the new one starts when it redeploys, instead of overlapping the two, so a redeploy pauses the server for a moment.
# Profiles
Control the tools, credentials, and settings based on context
## Introduction
A profile can:
* Override credentials
* Restrict available servers and tools (see also [Limiting Severs](/limiting-servers))
* Rename tools and promote tools so they stay visible in Code Mode
* Force [Code Mode](/code-mode) to be enabled
A profile can be enabled by:
* direct assignment to user, service account or a team
* setting a querystring parameter: `https://gatana.ai/mcp?p=PROFILE_ID`
* sending a header: `x-gatana-profile-id: PROFILE_ID`
* access token claims
* assigning it to your personal access token (PAT)
* attaching it to one of your [OAuth clients](/connected-clients)
## Creating Profiles
Organization owners can create and manage profiles by going to **Profiles** in the side menu. In this view you can see all existing profiles for your organization, and a button to create new profiles. In the detail view of a profile you can assign the servers which this profile has access to. Existing profiles can also be managed by their [maintainers](#profile-maintainers).
A profile automatically gets member-level permissions on each server assigned to it.
You can also manage profiles using the [Gatana Public API](/public-api-introduction).
### Force Code Mode
When creating or editing a profile, you can set the **Force Code Mode** flag, when enabled [Code Mode](/code-mode) will be enabled for all sessions with this profile. Note that all servers, not only the ones assigned directly to the profile, will be covered by code mode. This means that you could for example create one profile which only forces code mode, without having any servers directly attached to it.
The Code Mode tools are served by the built-in `codemode` server, so setting the flag automatically adds that server to the profile and keeps it there while the flag is set. If the `codemode` server is not installed for your organization yet, saving the flag installs it. Installing a server for the whole organization requires owner permissions — a profile maintainer who is not an owner gets an error and must ask an administrator to install the server first.
Tools that a profile [promotes](#promoted-tools) stay listed next to the Code Mode tools.
### Open to All Users
Take care when opening a profile up to your entire organization. There is a risk of providing unintended access to servers if this flag is enabled since profiles automatically grants member-level permissions to every server assigned to it.
When creating or editing a profile, you can set the **Open to All Users** flag:
* **Enabled**: Any user in your organization can apply this profile using the `p=` query string parameter, the header, by assigning it to a PAT, by attaching it to one of their connected clients, or assigning themselves.
* **Disabled**: The profile can only be applied via direct user assignment or by claim mappings from an external identity provider.
### Restrictive Mode
When enabled, sessions with the profile applied will only have access to the servers assigned to that profile. This is useful for limiting server availability in specific contexts, such as sub-agents or isolated workflows.
### Configuring Tools
For each server in a profile, you can control which tools are available, expose tools under different names, and promote tools so they stay visible in [Code Mode](/code-mode). This provides fine-grained control over tool access without needing to create separate server configurations.
To configure the tools of a server:
1. Go to the profile detail view
2. In the servers table, click **Edit** on the server row
3. Configure the tool selection, exposed names and promotions
4. Click **Save Changes**
The same page also manages the profile's credentials for that server.
#### Tool Selection
**Use every tool of the server** is enabled by default: every enabled tool of the server is available when the profile is applied. Turn it off to pick the tools yourself with **Add tools** — only the picked tools are then available.
While you pick the tools yourself, **Add new tools automatically** controls what happens when the server starts to report tools that did not exist before. When enabled, new tools join the selection automatically; when disabled, new tools stay unavailable until a maintainer adds them. Disable it to gradually roll out new tools, or to keep an AI agent limited to a fixed set of capabilities.
**Behavior:**
* Tool selection is independent of [Restrictive Mode](#restrictive-mode) — it applies regardless of whether restrictive mode is enabled
* Tool selection applies to tool calls through the [public API](/public-api-introduction) the same way as over MCP
* When multiple profiles are active, tool selections are combined (union) — if Profile A allows `tool1` and Profile B allows `tool2`, both tools are available
* Server-level tool disabling always takes precedence — if a tool is disabled at the server level, it won't be available even if the profile selects it
* Removing a server from a profile discards the tool configuration for that server — if you add the server back later, it starts over with every tool enabled
#### Renaming Tools
Every tool has an **Exposed Name** — the name that MCP clients see and call. By default this is `_`. Rename a tool to give it a shorter or more task-specific name.
A renamed tool:
* is listed under the new name instead of its regular one
* stays callable under its regular `_` name as well
* must be named with letters, digits and hyphens only (up to 64 characters, no underscores), and the name must be unique within the profile
When two active profiles rename different tools to the same name, the profile with the highest [priority](#profile-ordering) keeps the name, and the other tool falls back to its regular name so it stays reachable.
#### Promoted Tools
A promoted tool stays listed when the session runs in [Code Mode](/code-mode), next to the Code Mode tools. Use this to keep a few important tools directly callable while everything else is reached through `execute_code`.
Renaming and promoting are independent: you can rename a tool without promoting it, and promote a tool without renaming it.
## Profile Maintainers
Every profile has a list of **maintainers**: the users who are allowed to manage that profile. Maintainers can:
* Edit the profile's name, description and flags (Open to All Users, Restrictive Mode, Force Code Mode)
* Add and remove servers, and [configure their tools](#tool-restrictions)
* Manage the profile's credentials (configure API keys or authorize OAuth)
* Manage claim mappings and direct assignments
* Add and remove other maintainers
* Delete the profile
When a profile is created, its creator is automatically added as the first maintainer. Organization owners can always manage all profiles, regardless of the maintainer list.
To manage maintainers:
1. Go to the profile detail view
2. Under **Maintainers**, click **Add Maintainer** and select a user
3. To remove a maintainer, click the delete button on the corresponding row
A profile must always have at least one maintainer (the last maintainer cannot be removed).
Being a maintainer of a profile grants the ability to manage that profile's credentials on all of its servers, even servers the maintainer is not otherwise a member of.
## Applying Profiles
### Direct Assignment (User, Service Account or Team)
Profiles can be assigned directly on users, service accounts and teams. A user can assign profiles to their own account, and organizational owners can assign profiles to *any* account. For teams the same logic applied for team maintainers. Owners also have the possibility of assigning the profile in *locked mode*. This prevents the profile from being removed by users, and will take precedent in any credential resolution conflict.
To assign a profile to a user:
1. In **Gatana**, navigate to **People & Accounts**, click on the **account**
2. In the detail view, under **Profiles** click on **Assign profile**
3. Select the profile you would like to assign. If you are an organization owner, you can chose to add it in **locked mode**
To assign a profile to a team:
1. In **Gatana**, navigate to **Team**, click on the **team**
2. In the detail view, under **Profiles** click on **Assign profile**
3. Select the profile you would like to assign. If you are an organization owner, you can chose to add it in **locked mode**
### Claim Mappings
Claim mappings allow you to automatically apply a profile based on claims from your external identity provider. When a user authenticates, Gatana checks their claims against configured mappings and applies the matching profile with the highest priority.
To configure claim mappings, go to the profile detail view and add mappings with a claim key and value.
Consider the following claims from your IdP:
```json
{
department: "engineering",
groups: ["engineering", "platform-team"],
branch: {
office: "london"
}
}
```
There are different ways of matching:
| Claim Key | Claim Value | Description |
| --------------- | ------------- | ------------------------------------------------------ |
| `department` | `engineering` | Direct property match |
| `groups` | `engineering` | Array claim - matches if "engineering" is in the array |
| `branch.office` | `london` | Nested property using dot notation |
### Personal Access Tokens
Profiles can also be assigned directly on to personal access token, either on a user's own PAT:s, or on a service account PAT:s. Only organization owner can manage service account PAT:s.
To assign a profile to *your own PAT*:
1. In **Gatana**, go to **Clients & API Keys** in the left sidebar
2. Under **Personal access tokens**, click **Edit** for one of your PAT:s
3. Select the profile(s) you would like to associate with the PAT
To assign a profile to a *service account's PAT*:
1. In **Gatana**, navigate to **People & Accounts**, click on the **service account**
2. In the detail view, under **Personal Access Tokens** click on **Edit** for one of the PAT:s
3. Select the profile(s) you would like to associate with the PAT
### Connected Clients
Profiles can also be attached to a [connected client](/connected-clients), an app that signs in through OAuth instead of holding a token. The profiles are applied on every request that client makes, in addition to the profiles you already hold, the same way a PAT's profiles are.
To attach a profile to a connected client:
1. In **Gatana**, go to **Clients & API Keys** in the left sidebar
2. Under **Clients**, click **Edit** for the client
3. Select the profile(s) you would like to attach
You can attach the same profiles you could assign to a PAT: profiles that are [open to all users](#open-to-all-users) or that you are otherwise allowed to manage. Disconnecting the client forgets the attachment.
## Credentials Resolution
When a tool is called, Gatana determines which credentials to use in the following order:
1. **Profile credentials**: Gatana checks active profiles in [priority order](#profile-ordering) and uses the first valid credentials for the server. The credential type must match the server's authorization method. For API keys, the number of keys must also match the server configuration.
2. **Server credential scope**: If no active profile has valid credentials, Gatana uses the scope configured on the server:
| Server scope | Credentials used |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **User-scoped** | The user's own credentials. If these are missing, Gatana asks the user to [authorize](/credentials#provide-the-credentials). |
| **Server-scoped** | The shared server credentials. |
Gatana does not switch between these scopes when credentials are missing. A user-scoped server cannot use shared server credentials in place of missing user credentials.
To share credentials with a group on a user-scoped server:
1. Add the credentials to a profile.
2. Assign the profile to the group.
### Profile Ordering
When multiple profiles are active at the same time, they are evaluated in a fixed priority order. The first profile (in this order) that has valid credentials for the server wins:
1. Profiles [directly assigned to the user](#direct-assignment) in **locked mode**
2. Profiles [directly assigned to one of the user's teams](#direct-assignment) in **locked mode**
3. Profiles directly assigned to the user (not locked)
4. Profiles directly assigned to one of the user's teams (not locked)
5. All other profiles — e.g. applied via the `p=` query string parameter, the `x-gatana-profile-id` header, [claim mappings](#claim-mappings), a [personal access token](#personal-access-tokens), or a [connected client](#connected-clients)
Within the same priority level, profiles are checked in a stable order (by profile name), so resolution is deterministic. In short: **locked assignments always beat unlocked ones, and user assignments always beat team assignments** at the same lock level.
# Introduction
Mange your Gatana organization using the API instead of using the Gatana Web App
Your can manage servers, credentials, users, teams, and organization settings using the Gatana public API. The API is an REST API and the requests can be authenticated by providing:
* an OAuth access token issued by Gatana,
* a Gatana Personal Access Token (PAT),
* or, if enabled, an OAuth access token issued by the organization's federated OIDC identity provider (see [Identity Federation](/federation#oidc-access-token-trust) for more information)
You can explore all available routes in the sidebar.
#### OpenAPI Specifications
* [https://api.gatana.ai/api/v1/openapi.json](https://api.gatana.ai/api/v1/openapi.json)
* [https://api.gatana.ai/api/v2/openapi.json](https://api.gatana.ai/api/v2/openapi.json)
# Quick Start
Get started in 5 minutes
# Create new organization and connect
Install the Gatana skill (recommended):
```bash
npx skills add gatana-ai/gatana
```
Then ask your agent:
```
Create a Gatana organization and connect this computer to it
```
Without installing skill:
```
Follow https://www.gatana.ai/llms.txt to create a Gatana organization and connect this computer to it
```
Skill source: [https://github.com/gatana-ai/gatana/tree/main/skills/gatana](https://github.com/gatana-ai/gatana/tree/main/skills/gatana).
```bash
curl -fsSL https://github.com/gatana-ai/gatana/releases/latest/download/install.sh | sh
gatana signup
gatana install
```
```bash
brew install gatana-ai/tap/gatana
gatana signup
gatana install
```
1. Go to [https://hello.gatana.ai/signup](https://hello.gatana.ai/signup)
2. Create your organization
**Install into harness**
```bash
claude mcp add --transport http --scope user gatana https://gatana.ai/mcp
claude mcp login gatana
```
```bash
codex mcp add gatana --url https://gatana.ai/mcp
codex mcp login gatana
```
Codex in the terminal, the IDE extension and the desktop app will all get Gatana.
Add this to `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"gatana": { "type": "http", "url": "https://gatana.ai/mcp" }
}
}
```
Cursor asks you to sign in the first time it connects.
Add this to `.vscode/mcp.json` in your workspace:
```json
{
"servers": {
"gatana": { "type": "http", "url": "https://gatana.ai/mcp" }
}
}
```
VS Code asks you to sign in the first time it starts the server.
```bash
hermes mcp add gatana --url https://gatana.ai/mcp --auth oauth
```
If Hermes is running, run `/reload-mcp`. You do not have to restart it.
Add this to `~/.openclaw/openclaw.json`:
```json
{
"mcp": {
"servers": {
"gatana": { "url": "https://gatana.ai/mcp", "transport": "streamable-http" }
}
}
}
```
Most clients take this shape:
```json
{
"mcpServers": {
"gatana": { "type": "http", "url": "https://gatana.ai/mcp" }
}
}
```
A client that only asks for a URL takes `https://gatana.ai/mcp`. The client signs in over OAuth. A client that cannot do OAuth needs a [personal access token](/connecting-to-gatana#personal-access-token).
Note: this does not install the skills, for that you need to use the Gatana CLI
***
# Connect to an existing organization
Add `--base-url https://your-gatana.corpo.acme` to all Gatana CLI commands.
Install the Gatana skill (recommended):
```bash
npx skills add gatana-ai/gatana
```
Then ask your agent:
```
Connect this computer to Gatana
```
Without installing skill:
```
Follow https://www.gatana.ai/llms.txt and connect this computer to Gatana
```
Skill source: [https://github.com/gatana-ai/gatana/tree/main/skills/gatana](https://github.com/gatana-ai/gatana/tree/main/skills/gatana).
```bash
curl -fsSL https://github.com/gatana-ai/gatana/releases/latest/download/install.sh | sh
gatana install
```
```bash
brew install gatana-ai/tap/gatana
gatana install
```
**Install into harness**
```bash
claude mcp add --transport http --scope user gatana https://gatana.ai/mcp
claude mcp login gatana
```
```bash
codex mcp add gatana --url https://gatana.ai/mcp
codex mcp login gatana
```
Codex in the terminal, the IDE extension and the desktop app will all get Gatana.
Add this to `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"gatana": { "type": "http", "url": "https://gatana.ai/mcp" }
}
}
```
Cursor asks you to sign in the first time it connects.
Add this to `.vscode/mcp.json` in your workspace:
```json
{
"servers": {
"gatana": { "type": "http", "url": "https://gatana.ai/mcp" }
}
}
```
VS Code asks you to sign in the first time it starts the server.
```bash
hermes mcp add gatana --url https://gatana.ai/mcp --auth oauth
```
If Hermes is running, run `/reload-mcp`. You do not have to restart it.
Add this to `~/.openclaw/openclaw.json`:
```json
{
"mcp": {
"servers": {
"gatana": { "url": "https://gatana.ai/mcp", "transport": "streamable-http" }
}
}
}
```
Most clients take this shape:
```json
{
"mcpServers": {
"gatana": { "type": "http", "url": "https://gatana.ai/mcp" }
}
}
```
A client that only asks for a URL takes `https://gatana.ai/mcp`. The client signs in over OAuth. A client that cannot do OAuth needs a [personal access token](/connecting-to-gatana#personal-access-token).
Note: this does not install the skills, for that you need to use the Gatana CLI
# Agentic Sandbox
Secure ephemeral containers for autonomous agents
The Agentic Sandbox is currently in **early access**. Features and APIs may change. To enable it for your organization, you can opt-in in the Sandbox page in the Gatana Dashboard.
The Agentic Sandbox provides secure, ephemeral Ubuntu containers for autonomous AI agents. Each sandbox is an isolated environment where agents can execute code, read and write files, and interact with the Gatana platform — all within a Kata Container virtual machine that is automatically cleaned up after a period of inactivity.
Sandboxes are designed for tasks like code generation and multi-step agent workflows where the agent needs a real Linux environment to operate in. One particular interesting case is developing and deploying [runnable code servers](/servers-runnable-code/) to make re-use available through the Gatana Gateway.
## Environment & Credentials
Each sandbox is a full Ubuntu 24.04 container with a comprehensive set of pre-installed tools:
| Category | Tools |
| -------------------- | ----------------------------------------------------- |
| **Languages** | Node.js 24, Python 3, pip, venv |
| **Package Managers** | npm, uv (Python) |
| **Dev Tools** | git, build-essential, TypeScript, pyright |
| **CLI Utilities** | curl, wget, jq, yq, ripgrep, fd-find, tree, nano, vim |
| **Database Clients** | postgresql-client, sqlite3 |
| **Networking** | openssh-client, net-tools, dnsutils |
The container runs as a non-root `ubuntu` user (uid 1000) with a home directory at `/home/ubuntu`.
### Gatana CLI & Automatic Authentication
The **Gatana CLI** (`gatana`) is pre-installed globally. On startup, the sandbox automatically authenticates with your organization's Gatana instance using OIDC token exchange — no manual configuration required.
This means:
* A `~/.gatana.config` file is written with connection details for the CLI (autmatically used by all Gatana SDK:s and CLI tools)
* The `GATANA_API_KEY` environment variable is set and inherited when using the `exec` API command (e.g. what LangChain Deep Agent uses)
* Credentials are automatically refreshed before they expire
Any command or script running inside the sandbox can immediately call Gatana APIs or use the CLI without setup.
Sandbox tokens are scoped to **read-only** permissions, with the reach of the user who created the sandbox. The sandbox can read the servers that user may use over MCP, the user's own teams, profiles, and its own user data. Calling the is also possible for those same servers.
## Shell Access
There are two ways to get interactive shell access to a sandbox: the **Web Terminal** in the dashboard, **Gatana CLI Tools** and **Direct SSH** from any SSH client.
### Web Terminal
Navigate to a sandbox's detail page in the Gatana dashboard. The integrated terminal connects automatically via WebSocket and provides a full xterm.js-based shell session. It supports window resizing and automatic reconnection.
### Gatana CLI Tools
```bash
gatana sandbox sandboxId shell
```
### Direct SSH
For access from your local terminal or an agent framework, you can connect via SSH using a short-lived session token.
**Step 1 — Obtain a session token:**
```bash
curl -X POST https://{org}.gatana.ai/api/v1/sandboxes/{sandboxId}/ssh-session \
-H "Authorization: Bearer $GATANA_PAT"
```
This returns a JSON response containing a short-lived JWT (valid for 5 minutes):
```json
{
"token": "eyJhbGciOiJSUz..."
}
```
**Step 2 — Connect using the token as your username:**
```bash
ssh -o StrictHostKeyChecking=no -p 2222 "eyJhbGciOiJSUz..."@{gateway-host}
```
Regarding `StrictHostKeyChecking`: this is a temporary requirement during the early-access period.
The SSH proxy authenticates the JWT, decrypts the per-sandbox SSH keypair, and bridges your session to the sandbox container. Each sandbox has a unique ed25519 keypair generated at creation time and encrypted at rest.
All SSH sessions — both web terminal and direct — are recorded in the sandbox audit log with start and close events.
## Deep Agent
Gatana sandboxes implement the [LangChain Deep Agent](https://github.com/langchain-ai/deepagents) sandbox backend interface. The `gatana-langchain` Python package provides a `GatanaSandbox` implementation that handles the full lifecycle automatically:
```python
from gatana_client import GatanaClient
from langchain_anthropic import ChatAnthropic
from deepagents import create_deep_agent
from gatana_langchain import GatanaSandbox
client = GatanaClient()
with GatanaSandbox(client=client) as backend:
agent = create_deep_agent(
model=ChatAnthropic(model="claude-sonnet-4-20250514"),
system_prompt="You are a coding assistant with sandbox access.",
backend=backend,
)
result = agent.invoke({
"messages": [
{"role": "user", "content": "Create a Python script that prints the Fibonacci sequence and run it"}
]
})
print(result["messages"][-1].content)
# Sandbox is automatically destroyed when the `with` block exits.
```
For the full integration guide, see [LangChain Deep Agents](/integration-examples/langchain-deepagent). The `gatana-langchain` package is available in the [gatana-python repository](https://github.com/gatana-ai/gatana-python/tree/main/packages/gatana-langchain).
## API
The sandbox exposes three core operations through the Gatana API: **exec**, **read file**, and **write file**. All operations are proxied through the Gatana backend and require authentication.
### Execute a Command
Run a shell command inside the sandbox. The response is streamed as newline-delimited JSON (NDJSON).
```bash
curl -X POST https://{org}.gatana.ai/api/v1/sandboxes/{sandboxId}/exec \
-H "Authorization: Bearer $GATANA_PAT" \
-H "Content-Type: application/json" \
-d '{"command": "python3 -c \"print(42)\"", "timeout": 30}'
```
**Request body:**
| Field | Type | Default | Description |
| --------- | ------ | -------------- | --------------------------------- |
| `command` | string | *(required)* | Shell command to execute |
| `timeout` | number | 300 | Maximum execution time in seconds |
| `workdir` | string | `/home/ubuntu` | Working directory |
**Response** (NDJSON stream):
```
{"type":"stdout","data":"42\n"}
{"type":"done","exitCode":0}
```
Each line is a JSON object with `type` being one of `stdout`, `stderr`, `done`, or `error`. The `done` event includes the `exitCode`.
### Read a File
Stream the contents of a file from the sandbox.
```bash
curl -X POST https://{org}.gatana.ai/api/v1/sandboxes/{sandboxId}/read-file?path=/home/ubuntu/output.txt \
-H "Authorization: Bearer $GATANA_PAT" \
--output output.txt
```
The file content is returned as `application/octet-stream`.
### Write a File
Write content to a file in the sandbox. Parent directories are created automatically.
```bash
curl -X POST https://{org}.gatana.ai/api/v1/sandboxes/{sandboxId}/write-file?path=/home/ubuntu/input.txt \
-H "Authorization: Bearer $GATANA_PAT" \
-H "Content-Type: application/octet-stream" \
--data-binary @local-file.txt
```
The request body is written directly to the specified path.
All exec, read, and write operations update the sandbox's last activity timestamp and are recorded as audit log events with full details (command, stdout/stderr, exit codes).
## Lifecycle & Limits
| Setting | Value |
| ------------------------ | ----------------------------- |
| Max concurrent sandboxes | 10 per organization |
| Inactivity timeout | 30 minutes |
| Container Isolation | Kata Containers |
| User | `ubuntu` (uid 1000, non-root) |
| Resource limits | 1 CPU, 1 GiB memory |
### Creating a Sandbox
**Via the dashboard:** Navigate to **Sandboxes** in the sidebar and click **Create Sandbox**.
**Via the CLI**
```bash
gatana create sandbox
```
### Destroying a Sandbox
**Via the dashboard:** Open the sandbox detail page and click **Destroy**.
**Via the CLI**
```bash
gatana delete sandbox sandboxId
```
Sandboxes that have been inactive for longer than the timeout are automatically destroyed. All sandbox activity is available in the audit log at `GET /api/v1/sandboxes/{sandboxId}/audit-logs`.
# Auto-Provisioning (SCIM)
Use SCIM 2.0 to sync users and teams into Gatana from your federated identity provider
## Introduction
How SCIM fits with SAML, OIDC and access rules is explained in the guide [How to add SSO, SAML and SCIM to MCP servers](https://www.gatana.ai/guides/how-to-add-sso-saml-scim-to-mcp-servers).
SCIM (System for Cross-domain Identity Management) is a standard protocol for syncing users and groups across systems. Gatana supports inbound SCIM; meaning that you can automate your user and team lifecycles in Gatana to keep accounts consistent across platforms. This also means that a user and group will exist in Gatana without even having used it.
Gatana’s inbound SCIM feature supports any client following SCIM 2.0 protocol. The following connection types have been verified working: Okta.
## Enabling SCIM
To enable SCIM for your organization:
1. Go to your organization's settings by clicking on **Settings** in the left side-menu
2. Scroll down to **SCIM**
3. Check **Enable SCIM 2.0**
4. Click on **Manage SCIM tokens..** and create a new token
5. *Optional:* Configure the deletion behaviour for users and teams
At this point, configure your identity provider with the SCIM URL `https://gatana.ai/api/scim/v2` and use the token from **Step 4** as *Bearer Authorization*.
# Secret Stores
Integrate with external secret management systems to securely inject secrets into server configurations.
## Introduction
Secret stores are read-only sources of secrets, allowing you to integrate Gatana with your organization's secret management infrastructure. Instead of storing sensitive credentials directly in Gatana, you can reference secrets from external providers like AWS Secrets Manager, Google Cloud Secret Manager, HashiCorp Vault, Infisical, or Azure Key Vault.
Secrets from external stores can be used anywhere you would use credentials:
* **HTTP headers**
* **Environment variables**
* **Command arguments**
* **Remote MCP URLs**
Additionally to those values, secrets also be used in:
* **Client ID & Client Secret** in a server's OAuth authorization configuration
## Supported Providers
Gatana supports four external secret store providers:
* **AWS Secrets Manager**
* **GCP Secret Manager**
* **HashiCorp Vault**
* **Infisical**
* **Azure Key Vault**
## Using Secrets
Reference secrets in your server configurations using the syntax:
```
{{ secrets.STORE_NAME.MY_SECRET_MAPPING }}
```
Secrets can also be accessed directly without creating a mapping:
```
{{ secrets.STORE_NAME.[arn:aws:secretsmanager:us-east-1:123456789:secret:my-secret] }}
```
References work in these server settings:
* The URL, headers and environment variables of the server
* The command of a local server
* The OAuth client ID and client secret
Gatana puts the value in exactly as the store holds it, with no escaping.
## How It Works
1. **Configure a Secret Store**: Connect Gatana to your external secret provider with the necessary credentials.
2. **Create Secret Mappings**: Map friendly names to secret identifiers in your external store.
3. **Reference Secrets**: Use the `{{ secrets.store_name.secret_name }}` syntax in your server configurations.
When a server needs a secret, Gatana fetches it from the external store at runtime. Secrets are cached for 1 minute to improve performance.
## Configuring Secret Stores
### AWS Secrets Manager
```json
{
"name": "my_aws_store",
"type": "aws_secrets_manager",
"configuration": {
"region": "us-east-1",
"accessKeyId": "AKIA...",
"secretAccessKey": "..."
}
}
```
The IAM user or role must have `secretsmanager:GetSecretValue` permission for the secrets you want to access. To give Gatana access to all secrets, attach the following policy:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AccessAllSecrets",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "*"
}
]
}
```
For more restrictive access, scope the policy to specific secret ARNs:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AccessSpecificSecrets",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:gatana/*"
]
}
]
}
```
### GCP Secret Manager
```json
{
"name": "my_gcp_store",
"type": "gcp_secret_manager",
"configuration": {
"projectId": "my-project-id",
"serviceAccountKey": "{...}"
}
}
```
The `serviceAccountKey` should be the JSON key file contents for a service account. The service account must have the `Secret Manager Secret Accessor` role (`roles/secretmanager.secretAccessor`).
To grant access to all secrets in a project:
```bash
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
--role="roles/secretmanager.secretAccessor"
```
For more restrictive access, grant the role on specific secrets:
```bash
gcloud secrets add-iam-policy-binding SECRET_NAME \
--member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
--role="roles/secretmanager.secretAccessor"
```
### HashiCorp Vault
```json
{
"name": "my_vault_store",
"type": "hashicorp_vault",
"configuration": {
"address": "https://vault.example.com",
"token": "hvs.xxx",
"namespace": "admin",
"mountPath": "secret"
}
}
```
| Field | Required | Description |
| ----------- | -------- | --------------------------------------------------- |
| `address` | Yes | The Vault server URL |
| `token` | Yes | A Vault token with read access to the secrets |
| `namespace` | No | Vault namespace (Enterprise feature) |
| `mountPath` | No | KV secrets engine mount path (defaults to `secret`) |
Gatana uses the KV v2 secrets engine.
### Infisical
```json
{
"name": "my_infisical_store",
"type": "infisical",
"configuration": {
"siteUrl": "https://us.infisical.com",
"accessToken": "...",
"projectId": "...",
"environment": "prod",
"secretPath": "/"
}
}
```
| Field | Required | Description |
| ------------- | -------- | ----------------------------------------------------------- |
| `siteUrl` | Yes | Infisical instance URL (e.g. to `https://us.infisical.com`) |
| `accessToken` | Yes | The access token |
| `projectId` | Yes | The Infisical project ID |
| `environment` | Yes | Environment slug (e.g. `prod`) |
| `secretPath` | Yes | Path within the project (e.g. `/`) |
### Azure Key Vault
```json
{
"name": "my_azure_store",
"type": "azure_key_vault",
"configuration": {
"vaultUrl": "https://my-vault.vault.azure.net",
"tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientSecret": "..."
}
}
```
| Field | Required | Description |
| -------------- | -------- | ------------------------------ |
| `vaultUrl` | Yes | The Azure Key Vault URL |
| `tenantId` | Yes | Azure AD tenant ID |
| `clientId` | Yes | App registration client ID |
| `clientSecret` | Yes | App registration client secret |
The app registration must have permission to read secrets from the Key Vault. If using Azure RBAC (recommended), assign the `Key Vault Secrets User` role:
```bash
az role assignment create \
--role "Key Vault Secrets User" \
--assignee APP_CLIENT_ID \
--scope /subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/VAULT_NAME
```
If using access policies instead of RBAC, grant `Get` permission for secrets:
```bash
az keyvault set-policy \
--name VAULT_NAME \
--spn APP_CLIENT_ID \
--secret-permissions get
```
## Secret Mappings
Secret mappings create friendly aliases for secrets in your external stores. This provides several benefits:
* **Abstraction**: Change the underlying secret without updating server configurations
* **Simplified syntax**: Use short names instead of ARNs or long paths
* **Access control**: Only expose specific secrets to Gatana
### Creating a Mapping
```json
{
"name": "openai_key",
"secretIdentifier": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/openai-api-key"
}
```
The `secretIdentifier` format depends on the provider:
| Provider | Identifier Format |
| ------------------- | ------------------------------------------------------------------------------ |
| AWS Secrets Manager | ARN or secret name |
| GCP Secret Manager | `projects/{project}/secrets/{name}/versions/{version}` or just the secret name |
| HashiCorp Vault | Path within the mount (e.g., `myapp/config`) |
| Infisical | The secret key name |
| Azure Key Vault | The secret name |
# OpenAPI & Swagger
Turn any OpenAPI or Swagger specification into an MCP server
## Introduction
OpenAPI/Swagger servers let you expose an existing REST API as MCP tools without writing any code.
Gatana reads an [OpenAPI](https://www.openapis.org/) or Swagger specification and automatically
generates one tool per API operation. When an AI client calls a tool, Gatana translates the call
into the corresponding HTTP request and returns the response.
Both **OpenAPI 3.x** and **Swagger 2.0** specifications are supported.
## Providing the Specification
There are two ways to supply the specification:
* **Remote Spec URL:** Provide a URL to a hosted OpenAPI/Swagger document. Gatana fetches the spec
from this URL and keeps it updated automatically, so changes to your API are picked up over time.
* **Manual Spec:** Paste the specification directly. Use this when the spec is not publicly reachable
or when you want to pin an exact version.
When using a remote URL, you can use the **Test** button to validate the spec before saving. It reports
the API title and version, the detected spec version, the number of operations found, and a preview of
the resolved endpoints.
## Configuration Options
* **Spec URL / Spec** — The source of the OpenAPI/Swagger document (see above).
* **Base URL Override (Optional)** — By default the base URL is derived from the spec (the `servers`
entry for OpenAPI 3.x, or `host` + `basePath` + `schemes` for Swagger 2.0). Set this to override the
target endpoint, for example to point at a staging environment. Trailing slashes are stripped.
* **Headers** — Custom HTTP headers sent with every request, useful for authentication (e.g. an
`Authorization` header) or other fixed values your API requires.
See the [Credentials page](/credentials) for more information about configuring authorization and credentials.
## How Tools Are Generated
Each operation (path + HTTP method) in the specification becomes a single MCP tool:
* **Tool name** — Derived from the operation's `operationId` (sanitized to a valid tool name). If no
`operationId` is present, a `{method}_{path}` name is generated, and any collisions are de-duplicated.
* **Title and description** — Taken from the operation's `summary` and `description`.
* **Input schema** — Built from the operation's `path`, `query`, and `header` parameters. If the
operation defines a request body, a required `body` property is added. Path-level parameters are merged
in, and operation-level parameters override them. Local `$ref`s to `components`/`definitions` are
kept as `$defs` entries, but only the definitions the operation actually uses, so a tool from a
large specification stays small.
* **Output schema** — Taken from the operation's first successful JSON response when it describes an
object. Arrays are wrapped as `{ result: [...] }`; other response types produce no output schema.
* **Schema dialects** — Keywords from Swagger 2.0, OpenAPI 3.0 and older JSON Schema drafts are
rewritten to JSON Schema 2020-12: `nullable` becomes a `null` type, boolean
`exclusiveMinimum`/`exclusiveMaximum` become bounds, and a `pattern` that is not a valid JavaScript
regular expression is dropped. One malformed definition therefore does not stop the whole
specification from converting.
When a tool is invoked, Gatana substitutes path parameters, appends query parameters (arrays become
repeated query values), applies header parameters and configured headers, serializes any JSON request
body, and performs the HTTP request against the base URL. JSON responses are parsed and returned; a
non-2xx response results in an error that includes the response body.
# FaaS Runnable Code
Execute abitrary code and expose as tools
## Introduction
When a function is the right tool, and how hosted functions compare with a self-written server or a Cloudflare Worker, is covered in the guide [How to run serverless functions as MCP tools](https://www.gatana.ai/guides/run-serverless-functions-as-mcp-tools).
Runnable code servers, also known as hosted servers, similar to servers based on [executable apps](/servers-standard-mcp), the difference is that instead of starting a pre-packaged executable application, you can provide source-code which methods can be exposed as MCP tools.
Two runtimes are supported: **Node.js 24** and **Python 3.13**. You select the runtime when you create the server. You can change it later under **Server Settings**, which redeploys the server; the source package then has to match the new runtime.
## Writing Source Code
There are three methods to upload your source code:
* **Directly in the editor:** You can edit files directly in the embedded code editor above. Changes are saved and deployed when you click the "Deploy" button.
* **Upload ZIP archive:** Prepare a ZIP file containing your source code and upload it using the "Upload Archive" button. This will replace the existing code with the contents of the ZIP file.
* **Gatana CLI:** Use our command line tool to upload and manage your function's source code. See the [GitHub repository for the CLI tool](https://github.com/gatana-ai/gatana-js/) for details. The `hosted init`, `hosted verify` and `hosted run` commands work with Node.js packages; `hosted upload` works with both runtimes.
### Quick-Start
```bash
curl -fsSL https://github.com/gatana-ai/gatana/releases/latest/download/install.sh | sh
mkdir pkg
cd pkg
gatana faas init
gatana faas upload my-hosted-server --create
# new tools are now available in the Gateway
```
### Validate and Test Locally
To ensure that the hosted server will be accepted by the Gateway, you can run:
```bash
gatana faas verify .
```
You can test a tool as well:
```bash
$ gatana faas run . add -p a=1 -p b=2
Running tool "add"...
3
```
### Javascript Quick Start
Below is an example for a simple server which exposes two tools.
```javascript showLineNumbers
export const schema = {
whoami: {
description: 'returns HTTP headers',
},
add: {
description: 'adds two numbers',
input: z.object({
a: z.number(),
b: z.number(),
})
},
};
export function whoami(args, credentials) {
return JSON.stringify(credentials)
}
export function add({ a, b } = params) {
return String(a + b + 10)
}
```
You can generate a valid hosted server by running:
```bash
gatana faas init
```
### Python Quick Start
The same server in Python. The file is named `main.py`; tool inputs are declared with a [pydantic](https://docs.pydantic.dev/) model or a JSON Schema dict.
```python showLineNumbers
import json
from pydantic import BaseModel
class AddInput(BaseModel):
a: float
b: float
schema = {
"whoami": {"description": "returns HTTP headers"},
"add": {"description": "adds two numbers", "input": AddInput},
}
def whoami(args, credentials):
return json.dumps(credentials)
def add(args, credentials):
return str(args["a"] + args["b"] + 10)
```
Tool functions can be `async def` as well. A return value that is not a string is serialized to JSON.
### Structure
Your source-code package needs one entry file in its root. The runtime decides which one is loaded:
| Runtime | Entry file | Minimum content |
| ----------- | ---------- | -------------------------- |
| Node.js 24 | `index.js` | `export const schema = {}` |
| Python 3.13 | `main.py` | `schema = {}` |
To add tools, expand the `schema` object. Each key names a tool, and the module must export a function with the same name:
```javascript
export const schema = {
helloworld: {
description: 'returns hello world',
},
};
export function helloworld(args, credentials) {
return 'Hello World'
}
```
Each tool will be provided with two arguments:
The **args** argument is an object containing the arguments passed to the tool. In Python this is a `dict`; when the tool declares a pydantic model as `input`, the arguments are validated against it first.
The **credentials** argument is an object containing the credentials information. This is the structure:
```javascript
{
headers: {
authorization: 'Bearer OAUTH_ACCESS_TOKEN' // Automatically injected in case of OAuth method
}
apikeys: [
["key1", "value1"],
["key2", "value2"],
],
gatanaUserEmail: 'jane@example.com'
}
```
In Python the same object arrives as a `dict` with the keys `headers`, `apikeys` and `gatanaUserEmail`.
The `gatanaUserEmail` field holds the email address of the Gatana user the tool call is made for. Use it when your code needs to know who is calling, for example to look up the user in a downstream system. It is `null` (`None` in Python) when the call is not made on behalf of a user, for example when Gatana refreshes the tool list in the background.
### Environment Variables
The environment variables you configure in the **Server Settings** will be available in the `process.env` object in Node.js, and in `os.environ` in Python.
### Persistent Storage
The filesystem is reset on every deployment, so files your code writes do not survive a restart. To keep files across restarts and redeploys, enable [persistent storage](/persistent-storage) for the server and write to the directory named in the `GATANA_DATA_DIR` environment variable.
### Private Networks
Your code reaches the public internet only. To reach a database or an API on your own network, enable [Tailscale](/tailscale) for the server. It then connects to hosts on your tailnet over ordinary sockets, with no change to your code.
### Package Management (npm)
You can use `npm` to manage your packages. Run `npm install` to install dependencies and make sure your `node_modules` folder is part of the code package.
### Package Management (Python)
Add a `requirements.txt` to the root of your package. It is installed with [uv](https://docs.astral.sh/uv/) every time the server starts, before your code is loaded. Installation has to finish within about a minute, so keep the list to what the tools need.
The runtime already provides `mcp`, `pydantic`, `starlette` and `uvicorn`; your requirements are installed into the same environment, so a pin that conflicts with these can stop the server from starting. Check the deployment logs when a start fails.
As an alternative that needs no network access at start, place pure-Python packages in a `vendor` folder in the root of your package (for example with `pip install --target vendor -r requirements.txt`). The folder is added to the import path.
## Deployment and Output Logs
When you create, update a server, or upload a new source-package, Gatana will automatically deploy the server. You can click on **Deployment logs** in the Gatana App to see the status of the current revision deployment.
The **output logs** can be viewed in the server details view, under the **Server Logs** section. In the dropdown, select a revision, and the browser will begin stream the logs to your browser.
## Environment and Isolation
The operating system is [Debian Bookworm](https://www.debian.org/releases/bookworm/). Each local server is being executed in virual machine environment using [Kata Containers](https://katacontainers.io/), which provides a very strong layer of security and isolation.
# Standard MCP
Add any standard MCP server. Over HTTP (remote) or shell command (e.g. uvx, npx)
## Introduction
Standard MCP servers come in two variants:
* **Remote HTTP:** connect to an MCP server that is publicly accessible on the internet
* **Executable App:** run an `uvx` or `npx` MCP server as a **Local** server in your Gateway
## Remote HTTP
Remote servers are MCP servers that are publicly accessible over the internet. These servers are already
running and hosted by third parties, so you simply need to connect to them via their URL. Remote servers
in Gatana support the following transports:
* **HTTP Streaming**
* **Server-Sent Events (SSE)**
The following configuration options are available:
* **URL**
* **Headers**
See the [Credentials page](/credentials) for information about how to configure authorization and credentials.
## Executable Apps
Many MCP servers today are provided either as an `uvx` or `npx` command. These one-liners
download and install a Python or NodeJS application and start it. These are fully supported in Gatana.
The server application may provide one of the following transports:
* **Standard I/O:** the common transport for local servers
* **HTTP Streaming or SSE:** some local servers use HTTP as a transport. This is particularly useful when using OAuth credentials
The following configuration options are available:
* **Command** (for example `uvx workspace-mcp`)
* **Environment Variables**
* **Headers** (in case of HTTP transport)
* **Files** (the provided files will be available in the same folder as the app is running)
* **Persistent Storage** (a data volume at `/data` that keeps files across restarts, see [Persistent Storage](/persistent-storage))
* **Tailscale** (lets the app reach hosts on your private tailnet, see [Tailscale](/tailscale))
Use `{{ credentials.keys.MY_API_KEY }}` to access an API key.
There is no template expression for OAuth access tokens. Gatana sends the token in the `Authorization` header on each request. The app must use HTTP Streaming or SSE to receive it.
See the [Credentials documentation](/credentials) for information about credentials.
### Deployment and Output
When you create or update a server, Gatana will automatically deploy the server. You can click on **Deployment** tab in
the Gatana App to see the status of the current revision.
The **output** can be viewed in the server details view, under the **Server Logs** section. In the dropdown, select a revision, and the browser will begin to stream the logs to your browser. When the server runs next to a sidecar (for example [Tailscale](/tailscale)), a **Container** selector above the log view picks whose log to read.
### Environment and Isolation
The operating system is [Debian Bookworm](https://www.debian.org/releases/bookworm/). Each local server is executed in a virtual machine environment using [Kata Containers](https://katacontainers.io/), which provides a very strong layer of security and isolation.
## Tailscale VPN
Gatana supports connecting to Tailscale, so you can access services inside your Tailnet. For more information, see [Tailscale Docs](/tailscale).
# SIEM Streaming
Stream Gatana audit events to your own SIEM as HMAC-signed NDJSON batches, over HTTPS or into an S3 or Cloud Storage bucket.
## Introduction
A step-by-step version with receiver recipes and the delivery guarantees in one table is the guide [How to get audit logs of every MCP tool call into your SIEM](https://www.gatana.ai/guides/mcp-audit-logs-to-siem).
SIEM streaming delivers your organization's audit events to a security platform you control. Gatana pushes events to a destination you configure, so your own detection rules, dashboards, and retention apply to Gatana activity alongside the rest of your estate.
Choose a destination:
| Kind | What Gatana does | Typical use |
| ------------------------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **HTTPS endpoint** | `POST`s each batch to your URL | Splunk HTTP Event Collector, CrowdStrike Falcon LogScale, Elastic, Microsoft Sentinel through a relay, in-house pipelines |
| **S3 bucket** | Writes each batch as one object into a bucket on Amazon S3 or any S3-compatible store | Splunk SQS-based S3 input, Microsoft Sentinel S3 connector, Datadog, Elastic, Athena, long-term archive |
| **Cloud Storage bucket** | Writes each batch as one object into a Google Cloud Storage bucket | Google SecOps feeds, BigQuery external tables, Elastic, long-term archive |
Configure it under **Settings → SIEM Streaming** in the dashboard. One destination per organization. The feature requires a paid plan.
## Event sources
Two sources are streamed, and each can be turned off independently.
| Source | Contents |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `audit_logs` | Server, profile, team, and organization configuration changes; sign-ins; user and team membership changes; secret fetches; and every MCP request, including tool calls |
| `audit_logs_credentials` | Credential creation, updates, and deletion, plus automatic and manual OAuth token refreshes |
How much detail MCP events carry is controlled by the MCP audit log level in your organization settings; see [Audit Logs](/audit-logs#logging-detail) for the levels. At `terse`, the default, an event records which tool was called, how long it took, and which credential made the call.
Internal events visible only to Gatana staff, such as billing webhook traffic, are never streamed.
## Envelope schema
Each event is one JSON object on its own line.
```json
{
"id": "audit_logs:184223",
"version": 1,
"time": "2026-08-04T09:52:04.118Z",
"tenant": "acme",
"source": "audit_logs",
"action": "mcp_server.update",
"actor": { "type": "user", "id": "usr_8Fq2..." },
"entity": { "type": "mcp_server", "id": "srvr_pQ1..." },
"details": {}
}
```
| Field | Description |
| --------- | ------------------------------------------------------------------------------------------------------------------ |
| `id` | Stable, unique, and repeatable for the same event. Use it to deduplicate |
| `version` | Envelope version. A new version is introduced only for a breaking change |
| `time` | When the event happened, ISO 8601 UTC |
| `tenant` | Your organization identifier |
| `source` | `audit_logs` or `audit_logs_credentials` |
| `action` | Dotted action name, described below |
| `actor` | `{ "type": "user", "id": "..." }`, or `{ "type": "system", "id": null }` for background work and database triggers |
| `entity` | What the event is about. Either field can be `null` |
| `details` | Event-specific payload. Shape varies by action |
## Event catalog
Most actions follow `{entity type}.{event name}`, so an action appears for a new kind of event without any change on your side. Write rules against the prefix where you can.
| Action | Meaning |
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mcp_server.create` / `.update` / `.delete` | An MCP server was added, changed, or removed |
| `mcp_server.start` / `.stop` | A hosted server was started or stopped |
| `mcp_server.credentials.upsert` / `.delete` / `.delete_all` / `.copy` | Server credentials were changed |
| `profile.create` / `.update` / `.delete` | A profile was changed |
| `team.create` / `.update` / `.delete` | A team was changed |
| `team.member_join` / `.member_leave` | Team membership changed |
| `tenant_configuration.update` | Organization settings changed |
| `siem_destination.create` / `.update` / `.delete` | This streaming configuration itself changed |
| `alert_rule.create` / `.update` / `.delete` | An [alert rule](/alerts-and-notifications) was changed |
| `alert_rule_channel.create` / `.delete` | A channel was added to or removed from the selected channels of an alert rule |
| `notification_channel.create` / `.update` / `.delete` | A notification channel was changed |
| `connected_client.create` / `.update` / `.delete` | A user's OAuth client connection appeared, was configured, or was disconnected |
| `connected_client_profile.create` / `.delete` | A profile was attached to or detached from a connected client |
| `user.login_success` | A user signed in |
| `user.created` / `.disabled` / `.disabled_due_to_seat_limit` | A user account changed |
| `secret_store.secret_fetched` | A secret was read from an external secret store |
| `firewall.deny` / `firewall.log` | Egress from a hosted server was blocked or recorded |
| `mcp.tools/call` | A tool was called. `details.outcome` says how it ended (`ok`, `denied`, `not-found`, `needs-credentials`, `credentials-rejected`, `upstream-error`, `invalid-arguments`, `failed`, `unauthenticated`), and a `denied` event carries `details.reason`; see [Audit Logs](/audit-logs#outcome-of-a-call). Other JSON-RPC methods appear under the same `mcp.` prefix |
| `credential.create` / `.update` / `.delete` | A credential record changed |
| `credential.auto_oauth_refresh_success` / `.auto_oauth_refresh_failed` | Gatana refreshed an OAuth token in the background |
| `credential.manual_oauth_refresh_success` / `.manual_oauth_refresh_failed` | A user refreshed an OAuth token |
| `credential.expired_no_refresh` | An access token expired with no refresh token available |
| `credential.rejected_by_upstream` | The upstream MCP server rejected the access token; Gatana tries to refresh and tries again |
| `gatana.test` | A test event sent from the dashboard |
Changes to the streaming destination arrive in the stream as `siem_destination.*`, so a rule can alert when somebody redirects your audit trail. Delivery bookkeeping such as the internal read position is not an event and produces nothing.
## Delivery
Each batch holds up to 500 events or about 1 MB, with one event per line. Export runs once a minute. A short delay lets pending database writes finish before export.
**HTTPS endpoint.** Batches are `POST`ed as `application/x-ndjson`. Respond `2xx` as soon as you have accepted the batch. Requests time out after 10 seconds, and redirects are not followed.
**S3 and Cloud Storage buckets.** Each batch becomes one object. Its key uses the event IDs and the date and hour of the first event:
```
/dt=2026-10-24/09/___.ndjson
gatana/audit_logs/dt=2026-10-24/09/acme_audit_logs_184001_184500.ndjson
gatana/audit_logs_credentials/dt=2026-10-24/09/acme_audit_logs_credentials_0192a4b0-…_0192a4b0-….ndjson
```
The default prefix is `gatana/`. The `dt=` segment is a Hive-style partition that Athena and BigQuery external tables read as a column. Compressed objects end in `.ndjson.gz`. Use this suffix to select gzip decompression; `Content-Encoding` is not set. Every object carries custom metadata, which S3 presents with an `x-amz-meta-` prefix:
| Metadata key | Value |
| ------------------------------------ | --------------------------------------------------------- |
| `gatana-signature` | The signature described below, over the uncompressed body |
| `gatana-source` | `audit_logs` or `audit_logs_credentials` |
| `gatana-first-id` / `gatana-last-id` | Row ids of the first and last event in the object |
| `gatana-event-count` | Number of events in the object |
**Delivery is at-least-once.** A batch that is accepted but whose acknowledgement is lost is sent again, so the same event can arrive twice. Deduplicate on `id`. For buckets, retries with the same events reuse the object key. A retry with more events gets a new key, leaving both objects in the bucket. Events from each source arrive in order.
If your endpoint fails, Gatana retries with a widening delay of 1, 5, 15, and then 60 minutes:
* after **1 hour** of failures the destination is marked failing in the dashboard
* after **24 hours** your administrators receive one email
* after **7 days** delivery is disabled and a second email is sent
Nothing is lost while a destination is failing or paused. The read position is kept, so reactivating resumes where delivery stopped, as far back as audit retention allows.
## Verifying signatures
Every batch carries a signature over the exact NDJSON body. An HTTPS receiver finds it in a request header; a bucket object carries it in its metadata under `gatana-signature`:
```
X-Gatana-Signature: t=1785825392,v1=6c3b...f19a
```
`t` is a Unix timestamp in seconds and `v1` is `HMAC-SHA256` of `"{t}.{raw body}"` keyed with your signing secret. The secret is shown once when you configure the destination and can be revealed again from the dashboard.
Verify against the raw body, before any JSON parsing. For a gzipped object, decompress first; the signature covers the plain NDJSON bytes. Reject a batch whose timestamp is outside a tolerance you choose; five minutes is a reasonable default for a live endpoint. For stored objects, allow for the delay between signing and reading the object.
```javascript
function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
if (!Number.isFinite(age) || age > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1 ?? '', 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
```
```python
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(int(time.time()) - int(parts["t"])) > tolerance_seconds:
return False
expected = hmac.new(
secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
```
Rotating the secret takes effect immediately, so update your receiver first or accept a short gap. If your receiver can check two secrets at once, add the new one before rotating.
## What is never sent
* **Secret values.** Tokens, passwords, client secrets, private keys, and API keys are replaced with `[REDACTED]` wherever they appear, at any depth.
* **Encrypted secret columns.** A change to a stored secret is reported as `{"changed": true}` and never decrypted. Encrypted configuration, such as a server's transport settings, is included so that you can see what changed, with any embedded credentials redacted inside it.
* **Anything Gatana cannot decrypt** appears as `` rather than blocking the stream.
A `details` payload larger than 64 KB is replaced with `{"_truncated": true, "_originalBytes": …, "preview": "…"}`. This mainly affects `verbose` MCP logging, where a whole tool response is recorded.
## Receiver setup
### HTTPS endpoint
The endpoint URL must use HTTPS. In Gatana Cloud it must also resolve to a public address: internal hostnames and private address ranges are refused, because a shared platform must not be pointed at infrastructure it should not reach. Self-hosted deployments have no such boundary, so they may stream to a collector on their own network, such as `https://splunk.internal/…` or a private address. HTTPS is required either way, so audit data never travels in the clear.
Use the optional auth header to carry whatever credential your collector expects, for example `Authorization: Splunk ` for Splunk HTTP Event Collector, or `Authorization: ApiKey ` for Elastic. The value is stored encrypted and never returned by the API.
**Splunk HTTP Event Collector.** Point the URL at `https://:8088/services/collector/raw` and set the auth header to `Authorization` with the value `Splunk `. The raw endpoint accepts newline-delimited JSON directly. Set `sourcetype` on the token or its input.
**CrowdStrike Falcon LogScale.** Use the HEC-compatible ingest endpoint of your repository, `https:///api/v1/ingest/hec/raw`, with `Authorization: Bearer `.
**Elastic.** Send to an HTTP input on your ingest pipeline, then split lines and parse each as JSON.
**Anything else.** Any endpoint accepting an authenticated HTTPS POST of newline-delimited JSON works. Verify the signature, split on newlines, parse each line, and deduplicate on `id`.
### S3 bucket
Create a bucket, or choose a prefix in one you have, and an IAM user whose only permission is to put objects under that prefix. Gatana needs no list or read access. Enter the bucket, region, prefix, access key id, and secret access key in the dashboard. The secret is stored encrypted and never returned.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::acme-audit/gatana/*"
}
]
}
```
For an S3-compatible store such as MinIO, OVH Object Storage, or Cloudflare R2, set the store's HTTPS endpoint and turn on path-style addressing. The endpoint follows the same rule as an HTTPS destination: public in Gatana Cloud, any address in a self-hosted deployment. Checksum headers that only Amazon S3 understands are left out when an endpoint is set.
Bucket encryption settings apply. Configure Object Lock, lifecycle rules, and replication in your storage provider.
### Cloud Storage bucket
Create a service account, grant it the **Storage Object Creator** role (`roles/storage.objectCreator`) on the bucket, and create a JSON key for it. Paste the key file into the dashboard together with the bucket and prefix. The key is stored encrypted and never returned; the dashboard shows the service account email so you can tell which identity is in use.
```sh
gcloud iam service-accounts create gatana-siem --project acme-logs
gcloud storage buckets add-iam-policy-binding gs://acme-audit \
--member serviceAccount:gatana-siem@acme-logs.iam.gserviceaccount.com \
--role roles/storage.objectCreator
gcloud iam service-accounts keys create gatana-siem.json \
--iam-account gatana-siem@acme-logs.iam.gserviceaccount.com
```
Object Creator cannot overwrite objects. Gatana creates each object only if its name is unused. A retry counts an existing object as delivered.
### Reading objects into your SIEM
| Platform | How |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Splunk | Splunk Add-on for AWS, SQS-based S3 input with an S3 event notification on the prefix. Source type: a custom JSON type with line breaking on newline |
| Microsoft Sentinel | Amazon S3 connector with an SQS queue on the prefix, or the Codeless Connector Framework for custom NDJSON |
| Datadog | Datadog Forwarder Lambda triggered by S3 object creation; it handles `.gz` |
| Elastic | Elastic Agent `aws-s3` input with SQS notifications, or the `gcs` input for a Cloud Storage bucket; both decode gzip and split lines |
| Google SecOps (Chronicle) | A Cloud Storage feed on `gs:///`, log type JSON |
| BigQuery or Athena | An external table over the prefix with Hive partitioning on `dt`, format newline-delimited JSON |
In every case, verify `gatana-signature` from the object metadata if your pipeline can, and deduplicate on `id` if you ever ingest the same object twice.
### Testing a destination
Use **Send test event** under Settings to deliver a single `gatana.test` event. For an HTTPS endpoint the dialog shows the HTTP status and the beginning of the response body, which is usually enough to identify a rejected credential or a wrong path. For a bucket, it shows the test object under `gatana-test/` or the storage error.
# Fundamentals
Skills allow you to share knowledge and how to solve problems with your organization
## Introduction
Teams build up knowledge about how work is done: how to deploy a service, how to review a pull request, how to investigate an error. Today that knowledge is written down for AI agents as skills, and each agent reads them from a folder on the developer's machine. The folders drift apart, a new team member starts with none, and nobody can say which version is in use. Gatana stores the skills of your organization in one place, writes them into the folders your agents read, and keeps those folders in sync.
**Quick Start**
1. Create your skills in Gatana, or [push](/skills-push) the ones you already have.
2. On each machine that needs them: [`gatana skills install`](/skills-install).
3. They are kept up to date automatically.
For the team view, including where Claude Code, Codex and the other agents read skills and how a git repository compares, see the guide [How to share skills across Claude Code, Codex and other agents](https://www.gatana.ai/guides/share-skills-across-claude-code-and-codex).
## Definitions
* **Skill**: a Markdown document that tells an agent how a job is done in your organization. It has a name, a one-line description of when to use it, and the instructions.
* **`SKILL.md`**: the file format agents read. A folder named after the skill, holding a `SKILL.md` with the name and description in the frontmatter and the instructions as the body. Gatana uses the same format, so a skill travels between the two unchanged.
* **Collection**: a named group of skills. It is installed and shared as one unit.
* **Skills folder**: the directory an agent reads skills from, such as `~/.claude/skills` for Claude Code or `~/.agents/skills` for Codex, Cursor and others.
* **Hook**: a command an agent runs at the start of a session. Gatana installs one that refreshes the skills folders.
* **Share**: a grant that lets a person or a team read or maintain a private skill or collection.
## What is a skill in Gatana?
A skill in Gatana is the source of truth for one `SKILL.md`. Agents never read it from Gatana directly in the normal case: the CLI installs a copy into each skills folder, and the session-start hook refreshes the copy before the agent starts work. An agent that has no skills folder reads the same skill over MCP, through the [built-in Skills server](/skills-over-mcp).
The body is stored encrypted. The name and description are stored in plain text, so lists are answered without opening any skill.
## Why use skills in Gatana?
* **One copy.** A skill is written once and every machine and agent gets the same version. An edit reaches everyone at the next session start.
* **Every agent.** Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Copilot, Amp and Hermes read from their own folders. Gatana writes to all of them.
* **Access control.** A skill is readable by the whole organization, or private and shared with chosen people and teams. Agents acting for a person read what that person can read.
* **Two directions.** Skills can be written in Gatana, or edited locally and [pushed back](/skills-push). An agent can save a skill it has just worked out with the [MCP tools](/skills-over-mcp).
* **No repository to maintain.** No clone, no symlinks, no pull before each session.
## What your team experiences
Consider a release checklist that one developer has written as a skill.
### Your team does not use Gatana
1. The skill lives in `~/.claude/skills/release-checklist` on one machine.
2. The developer pastes it in chat, or commits it to a repository, and asks the others to copy it into their own folders.
3. Each colleague copies it to the folder of each agent they use.
4. The developer improves the checklist. The copies do not change.
5. A new team member joins and starts with an empty skills folder.
*Each machine ends up with its own version, and no one knows which one an agent followed.*
### Your team uses Gatana
1. The developer runs `gatana skills push`. The skill is now in Gatana, readable by the organization.
2. Each colleague runs `gatana skills install` once. The skill lands in every agent folder on their machine, and a hook keeps it there.
3. The developer improves the checklist in Gatana, or edits the local copy and pushes again. Every colleague has the new version at their next session start.
A new team member runs `gatana skills install` and has every skill they are allowed to read.
## Overview of the parts
### Skill
| Field | Rule |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | Lowercase letters, digits and single dashes, 1 to 64 characters. The same rule as a Claude Code `SKILL.md` name. Unique in the organization. |
| **Description** | One line, at most 1024 characters, written for an agent that decides whether the skill applies. Say when to use it, not what it contains. |
| **Instructions** | Markdown, at most 200 KiB. A skill is read into the agent's context whole, so keep it to instructions. |
| **Access** | `Organization` or `Private`. See [Permissions](#permissions). |
| **Collection** | The collection the skill is in, or none. |
### Collections
A collection is a named group of skills. It follows the same name rule as a skill, so a collection name is a valid folder name wherever a skill name is. You can install a whole collection with the CLI, and share it as one unit. Deleting a collection does not delete the skills: they move to the root level.
### Permissions
You set the base level of access of a skill or a collection:
| Access | Who can read it |
| ---------------- | ------------------------------------------------------ |
| **Organization** | Every member (default) |
| **Private** | The creator and the people and teams it is shared with |
A private skill or collection can be shared with individual accounts or teams. Each share has a level:
| Level | What it gives |
| ---------------- | -------------------------------------------------------------- |
| **Can read** | Read the skill, and agents acting for that person read it |
| **Can maintain** | Edit, share, change access, add and remove maintainers, delete |
A skill in a collection is readable by everyone who can read the collection, in addition to its own shares. Organization owners can maintain every skill.
### Installed folders
`gatana skills install` writes the skills into the folder of each agent on a machine and adds a hook that refreshes them at every session start. See [Syncing into Agents](/skills-install).
### The Skills server
Agents that cannot read a folder reach the same skills over MCP, through the built-in **Skills** server. See [MCP Tooling](/skills-over-mcp).
Get the skills onto a machine with [Syncing into Agents](/skills-install), put your existing skills into Gatana with [Syncing into Gatana](/skills-push), or give agents without a skills folder access with [MCP Tooling](/skills-over-mcp).
# Installing Skills
Sync the skills of your organization into the folders your agents read, and keep them up to date
## Introduction
Agents read skills from a folder on the machine they run on. When you run `gatana skills install`, this happens:
1. Skills are downloaded into your storage (e.g. `~/.agents/skill`)
2. A hook is created in your agent (e.g. on `SessionStart`) to keep them up-to-date.
## Installing
```bash
curl -fsSL https://github.com/gatana-ai/gatana/releases/latest/download/install.sh | sh
gatana config login
gatana skills install # install all skills and the hooks
gatana skills install skill-123 --no-hooks # install specific skill without any hooks
```
The name can be a skill or a collection. When a skill and a collection share a name, add `--collection` or `--skill` to say which one you mean.
## Where the skills go
By default, skills are installed into these folders:
| Preset | Folder | Agents |
| -------- | ------------------ | ------------------------------------------------- |
| `claude` | `~/.claude/skills` | Claude Code |
| `agents` | `~/.agents/skills` | Codex, Cursor, Gemini CLI, OpenCode, Copilot, Amp |
| `hermes` | `~/.hermes/skills` | Hermes |
You can control this:
```bash
gatana skills install claude # install only for Claude
gatana skills install my-collection ./team-skills # to a specific folder
```
Each skill becomes a folder named after it, holding a `SKILL.md` with the name and description in the frontmatter. Gatana adds its own metadata to the frontmatter (the organization, the skill id, the collection and the last update), which is how later runs know what came from Gatana.
## Which skills are synced
Gatana saves your selection for each folder: all skills you can access, one collection, or one skill. Each sync updates that selection:
* Skills deleted from Gatana or no longer accessible to you are removed from the folder.
* Skills from other sources are kept.
* Skills you edited locally are kept, and the sync reports them. [Push your changes](/skills-push) to Gatana, or use `--force` to replace them with the Gatana version.
If a folder has no saved selection, a sync downloads all skills you can access. You can use `gatana skills sync` on a new machine without running install first.
## Keeping them up to date
The install adds a `SessionStart` hook to run `gatana skills sync --quiet` before the agent starts work. You can also run `gatana skills sync` at any time. It accepts the same targets as install; see [Options](#options) for the available flags.
To manage hooks:
* Run `gatana skills hook {agent}` to show the hook configuration.
* Add `--install` to save it to the agent's configuration.
* Run `gatana skills remove-hooks` to remove hooks from one or all agents.
```bash
$ gatana skills hook claude
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "gatana skills sync --quiet"
}
]
}
]
}
}
```
The `gatana` command must be on the agent's `PATH`. If the hook reports "command not found", install the CLI globally or use the full path to `gatana` in the hook configuration.
## Options
| Option | Effect |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--dry-run` | Show what would change without writing |
| `--no-hooks` | Do not add the session-start hooks (install only) |
| `--no-prune` | Keep skills locally that were removed from Gatana |
| `--reset` | Forget the previous state and install every skill you can read again (install only) |
| `--force` | Overwrite skills that did not come from Gatana or that you edited locally, and install into a directory that is not empty or is synced from another organization |
| `--quiet` | Print nothing on success; warnings and errors still go to stderr |
| `--org ID` | Use this organization from the config file, instead of the default |
`gatana skills ls` lists the skills you can read, with `--query`, `--collection` and `--collections` to narrow it.
# MCP Tooling
Let agents without a skills folder list, read and save skills through the built-in Skills server
## Introduction
Not every agent reads a skills folder. A hosted agent, a sandbox, an automation or a chat client has no `~/.claude/skills` to install into. These agents reach the same skills over MCP, through the built-in **Skills** server in your gateway. The tools act for the person or service account that is connected, so an agent lists and reads what that person can read.
The server also gives agents a way to save skills. An agent that has just worked out how a job is done can store the result for the next one.
## Adding the server
1. In **Gatana**, click **Add Server** and choose **Built-in**.
2. Pick **Skills**. The default slug is `skills`.
3. Grant access to the users and teams that should have the tools, as for any other [server](/tool-servers-fundamentals#members).
The server needs no credentials. Any gateway connection that has access to it sees its tools.
## The tools
The tools carry the server slug as a prefix. With the default slug:
| Tool | What it does |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `skills_list_skills` | Lists the skills the caller can read, grouped by collection. Each line gives the name, the description and when the skill was last updated. An optional `query` narrows the list to names and descriptions containing the text, an optional `collection` to one collection |
| `skills_get_skill` | Returns one skill by name as a `SKILL.md` document, with the name and description in the frontmatter |
| `skills_create_skill` | Saves a new skill: a name, a one-line description and Markdown instructions. Readable by the organization by default; `team_names` makes it private to the caller and those teams, `collection` puts it in an existing collection |
| `skills_update_skill` | Replaces the description or the instructions of a skill, or renames it with `new_name`. Only the fields given change. Needs **Can maintain** on the skill |
The same [name rule](/skills-fundamentals#skill) and size limits apply as in the dashboard and the CLI.
## Making agents look for skills
An agent only follows a skill it has read. Agents with a skills folder see the skill names and descriptions at session start. Over MCP, the agent has to call `skills_list_skills` first.
To make agents check for skills first, add one line to the project's
`CLAUDE.md` or the equivalent instructions file: "At the start of a task, call `skills_list_skills` and follow
any skill that applies."
# Sharing Skills
Send skills you wrote or edited on your machine back to Gatana
## Introduction
Skills are often written where they are used: in the skills folder of an agent, while working on a task. `gatana skills push` sends such a skill to Gatana, so it reaches the rest of the organization at their next session start. It works for a skill Gatana installed and you edited, and for a new folder you wrote yourself.
## Pushing
```bash
gatana skills push # everything that changed in the default folders
gatana skills push ~/.claude/skills/release-checklist # one skill folder
gatana skills push ./team-skills # a directory of skill folders
gatana skills push ./team-skills/deploy/SKILL.md # one SKILL.md
```
Without a path, the push looks in the Claude Code and `~/.agents` folders and sends every skill that changed since it was installed or last pushed. With a path, it sends that `SKILL.md`, that skill folder, or every skill folder in that directory.
A skill that does not exist in Gatana is created. A skill that exists is updated. The name comes from the `name` in the frontmatter of `SKILL.md`, and it must follow the [name rule](/skills-fundamentals#skill): lowercase letters, digits and single dashes, at most 64 characters. A folder named differently from its skill is reported, but pushed. The `description` in the frontmatter becomes the description in Gatana.
A skill Gatana installed carries its collection in the `gatana-collection` metadata of the frontmatter, so it stays in its collection when pushed. A file naming no collection is created at the root level, and an existing skill keeps its place. `--collection` overrides both.
## Options
| Option | Effect |
| ------------------------------ | ---------------------------------------------------------------------- |
| `--collection NAME`, `-c NAME` | Put the pushed skills in this collection |
| `--force` | Overwrite the server copy even when it changed after yours |
| `--dry-run` | Show what would be created or updated without writing |
| `--org ID` | Push to this organization from the config file, instead of the default |
## Conflicts
Two cases are reported as a conflict and left alone:
* The skill also changed in Gatana after your copy was installed.
* A skill with the same name exists in Gatana, but your file was never installed from it, so there is no baseline to compare against. This happens when you write a new folder by hand under a name that is already taken.
Run `gatana skills install` (or `gatana skills sync`) first to get the server version, apply your change again, and push. Push with `--force` to replace the server version with yours. One conflict does not stop the push: the other skills in the same run still go through, and the command exits with an error code so a script can notice.
## Who can push
Creating a skill needs the right to create skills in the organization. Updating one needs **Can maintain** on that skill, which the creator, the people it is shared with at that level, and the organization owners have. See [Permissions](/skills-fundamentals#permissions). A skill you cannot maintain is reported and skipped; the others in the same push still go through.
A pushed skill gets **Organization** access. Change it to **Private** in Gatana and share it with the people or teams that should read it. Pushing into a collection with `--collection` gives the skill the access of that collection.
# Slack
Give Slackbot access to all your tools
Slackbot can use your Gatana tools from a Slack conversation. You add a Slack app, choose how users connect to Gatana, and give them access to tools. Gatana applies their permissions and [profiles](/profiles), and records tool calls in the [audit log](/audit-logs). Gatana does not read Slack messages.
## Guide
In this guide, we will integrate a free Slack workspace with Gatana using Slack Identity.
In **Gatana** open **Integrations → Slack** in the left sidebar.
### 1. Create
1. Open [api.slack.com/apps](https://api.slack.com/apps).
2. Under **Your App Configuration Tokens**, click **Generate Token**. Select the workspace that will own the app.
3. Paste the token into **Configuration token** in Gatana. Click **Create Slack app**.
The token expires after 12 hours. Gatana uses it to create the app and does not save it. Gatana encrypts the app's credentials before storing them.
The default app name is ` Gatana`. Use **Customize the app** to modify any details.
**Create the app manually**
You can also create the app yourself. This means copy-pasting a JSON document that Gatana will generate for you. To do this, click on **Create the app manually** and follow the structions.
### 2. Install
Click **Add to Slack**. This will redirect you to Slack where you must consent to the installation and select a workspace.
On Enterprise Grid, **Install to Organization** connects all workspaces in that Slack organization.
### 3. Authentication Policy
Choose an account policy for each workspace. New workspaces use **Connect manually**.
| Policy | Outcome | Use it when |
| -------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------- |
| **Match by email** | Automatically link accounts based on email. | Slack SSO is used and/or trust is high |
| **Connect manually** | Each user must verify the link once. Slackbot guides them through this. | Increase explicitness of user account linking |
| **Shared service account** | One [service account](/people-and-teams#service-accounts) is used for everyone | Everyone needs the same tools and credentials |
**Match by email**
Requires providing a list of email domains to be allowed, such as `acme.com`. On the first request, Gatana reads the user's email from Slack. If the domain is allowed, Gatana connects the user to the matching account.
If no account exists, the **Automatically create accounts in Gatana for new emails** option controls the result. This option is on by default:
* **On**: Gatana creates a **Member** account.
* **Off**: Gatana does not create an account. The user must connect manually.
Gatana trusts the email address Slack provides. Without single sign-on (SSO), a Slack workspace owner can change a member's email. Allow only domains your workspace verifies. Use **Connect manually** if you do not control the workspace.
**Connect manually**
Slackbot gives each user a link to Gatana. The user signs in and confirms the connection once.
#### Optional: Profile
The optional workspace **Profile** applies to every Slackbot request, together with the user's own profiles. You do not need to assign it to each user. Existing restrictive profiles still apply. A restrictive workspace profile limits users who have no restrictive profile of their own. See [Applying Profiles](/profiles#applying-profiles).
Choose a [restrictive profile](/limiting-servers) with only the servers Slack needs. [Code Mode](/code-mode) is another option: it gives Slackbot two tools to work with a larger tool set.
For Slack identity, select the profile here. Profiles set through a URL, request header, or **Clients & API Keys** do not apply.
## Use Gatana from Slack
1. Open a direct message with **Slackbot**.
2. Click **Apps** and add your organization's app with **+**, for example **Acme Gatana**.
3. Ask Slackbot to use a tool.
For each tool, Slackbot asks you to **Allow once**, **Always allow**, or **Deny**. This also applies to tools that only read data. You can change your choice in the app's MCP settings.
### Manually connect your account
If you must connect manually, Slackbot gives you a link:
1. Open the link and sign in to Gatana.
2. Check the Slack user, workspace, Gatana organization, and Gatana account. Select **Use another account** if needed.
3. Click **Confirm**. Return to Slack and repeat your request.
Only confirm a link from your own Slackbot conversation. Each link works once and expires after 15 minutes. Ask Slackbot again if you need a new link.
The first result shows which Gatana account and profiles the connection uses.
### Remove a connection
Open **My Account → Slack connections** and click **Remove** for the connection. With **Match by email**, the next request connects you again. Ask your administrator to change the policy to prevent this.
### Limits set by Slack
* Each user can have up to 5 active MCP servers. Gatana counts as one.
* Tools must respond within 60 seconds. Slackbot stops longer calls.
## Troubleshooting
| Problem | What to do |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Workspace is not connected | Install the app in that workspace, or enable the workspace in Gatana |
| Request comes from a bot or deleted user | Use a personal Slack account. Gatana only accepts requests from people |
| Shared service account no longer exists | Select another service account in step 3 |
| Organization has reached its user limit | Remove an unused Gatana user, or use **Connect manually** with existing accounts |
| Gatana account is disabled | Enable it under **People & Accounts** |
| Workspace has no bot token | Install the app in that workspace again |
| Slack reports an error, but Gatana shows no request | Check that Slack can reach your Gatana URL. Compare the signing secret with **Basic Information** in Slack. If it differs, remove the app from Gatana, save it manually with the current secrets, and install it again |
**Remove app from Gatana** removes the app, all connected workspaces, and their user connections from Gatana. The app stays in Slack.
## How Slack identity works
1. Slack sends a signed request to your organization's MCP URL, such as `https://acme.gatana.ai/mcp`.
2. Gatana checks the signature and rejects requests older than five minutes.
3. Gatana uses the workspace's account policy to identify the Gatana user, then checks access to the tool.
4. Gatana removes the Slack identity details before sending the request to the tool server.
Gatana encrypts the app secrets and workspace bot tokens before storing them. Under [Clients & API Keys](/connected-clients), this connection appears as **Slackbot**.
## OAuth sign-in
Use this method if every user must sign in through OAuth. It requires a paid Slack plan. A Free workspace rejects the app with `team_not_eligible`.
You create this app in Slack. You do not use the **Slack** page in Gatana.
### Create the app
1. Open [api.slack.com/apps](https://api.slack.com/apps). Click **Create New App**, then **From a manifest**.
2. Select the workspace and paste the manifest below. For Gatana Cloud, use `https://gatana.ai/mcp`. For self-hosted Gatana, replace it with your public HTTPS MCP URL.
```json
{
"display_information": {
"name": "Gatana",
"description": "Your organization's tools, from Slackbot"
},
"features": {
"bot_user": {
"display_name": "Gatana",
"always_online": true
}
},
"oauth_config": {
"scopes": {
"bot": ["mcp:connect"]
}
},
"mcp_servers": {
"gatana": {
"url": "https://gatana.ai/mcp",
"auth_type": "dynamic_client_registration"
}
}
}
```
3. Click **Install to Workspace**, or **Install to Organization** on Enterprise Grid. Approve the `mcp:connect` permission.
4. Ask users to follow the steps below.
### Connect as a user
1. Open a direct message with **Slackbot**.
2. Click **Apps** and add **Gatana** with **+**.
3. Choose your Gatana organization, sign in, and approve the connection.
4. Ask Slackbot to use a tool.
Manage the connection under [Clients & API Keys](/connected-clients), where it appears as **Slack**. You can assign a profile, view tool calls, or disconnect it there. The [Slack limits](#limits-set-by-slack) and tool approval choices also apply.
Slack registers as an OAuth client automatically. See [How the sign-in works](/connecting-to-gatana#oauth-21). If Slack reports `invalid_client` after a year without use, change the server URL in Slack's app settings and then change it back. This makes Slack register again.
# Tailscale VPN
Let a server reach databases and APIs on your private Tailscale network
## Introduction
[Executable Apps](/servers-standard-mcp#executable-apps) and [Runnable Code](/servers-runnable-code) can be connected to Tailscale to provide access to private services inside your Tailnet.
Remote HTTP and OpenAPI servers run outside Gatana, so this setting does not exist for them.
## Turning It On
Open the server's **Settings** tab, find **Tailscale VPN**, and check **Connect to your tailnet**. Paste an auth key, and optionally a machine name. The server restarts to join the tailnet.
If the section is not there, Tailscale is not available in your environment. In the Gatana cloud service it is part of the paid plans. A [self-hosted installation](/on-premise-self-hosting) offers it only when it is [configured for it](/on-premise-self-hosting#configuration).
## The Auth Key
Create the key in your Tailscale admin console, under **Settings** and then **Keys**. The key is spent at the first registration: the server stores the machine identity it received and keeps it across restarts, redeploys and idle stops. A server that has joined does not care that its key expires later.
Three properties matter:
| Property | Why |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Tagged** | The tag carries the ACLs, so you can scope access. A machine that joins with a tagged key also gets key expiry disabled by default, so it never has to re-authenticate. |
| **Not ephemeral** | Gatana stops idle servers, sometimes for a long time. Tailscale removes an ephemeral machine that stays offline, and the removal throws the server's identity away. |
| **Reusable** | Optional. The key is normally spent once, but a reusable key lets the server register again by itself if you remove its machine from your admin console. |
Give the tag only the access the server needs. An MCP server often runs third-party code, and on the tailnet it reaches exactly what its tag is allowed to reach. Grant it the one database port it is there for, not your whole network.
For a key without a tag, open the machine in your admin console and choose **Disable key expiry**; otherwise the machine must re-authenticate when its node key expires, which is after 180 days by default.
To move the server to a different key, paste the new key into the same field. The server restarts and joins as a new machine; remove the old machine in your admin console.
## Reaching Hosts On Your Tailnet
Nothing in your server's code has to change. The tailnet is reachable over ordinary sockets, so plain database drivers work as well as HTTP clients:
```js
// A Postgres server on your tailnet, by its Tailscale address
const db = new Client({ host: '100.101.102.103', user: 'reader' });
// Or a host behind a subnet router, by its address on your own network
const res = await fetch('http://10.0.5.20:8080/internal/api');
```
Machine names from MagicDNS need one extra step. Your server keeps using Gatana's own name resolution, so ask the tailnet resolver for those names directly, at `100.100.100.100`. Addresses always work, so the simplest approach is to configure your server with the address of the host it needs.
The environment variable `GATANA_TAILSCALE` is set to `true` when the tailnet is attached, so code that runs both with and without it can tell the difference.
## Lifecycle And Limits
* The server keeps its machine identity: restarts, redeploys and idle stops all re-attach as the same machine. A self-hosted installation without persistent storage falls back to registering on every restart; there the key has to be reusable, and rotated before it expires.
* The machine identity is kept on a small volume of its own. It appears on the [Storage page](/persistent-storage#the-storage-page) as a Tailscale identity volume, does not count against the storage quota, and cannot be deleted while the server uses it. After a key change the old identity volume lingers for a while and can be deleted by hand.
* A server on the tailnet stops its old instance before the new one starts when it redeploys, so a redeploy pauses the server for a moment, the same way [persistent storage](/persistent-storage#redeploy-behavior) does.
* The tailnet comes up before your server starts, so its first connection already works.
* If the tunnel restarts on its own, open connections through it break and your server has to reconnect.
* The server cannot advertise routes, act as an exit node, or publish anything with `tailscale serve`.
## Troubleshooting
**The server never finishes starting.** Almost always the auth key, on the first join or after a key change. Gatana waits for the tailnet before it starts your server, so a key that is expired, already spent, or single-use leaves the deployment waiting. Open the **Deployment** tab and select the `tailscale (sidecar)` container above the log view to read what Tailscale itself reports, then paste a fresh key.
**You removed the machine from your admin console.** With a reusable key the server registers again on its own at the next start. With a spent or expired key it cannot; paste a fresh key.
**The server starts but cannot reach a host.** Check your tailnet ACLs for the key's tag. Look for the machine in your Tailscale admin console under the name from the settings, or `gatana-` by default, and confirm the tag it carries.
# Protections & Firewall
Prevent confidential data leaks and unauthorized tool calls
## Introduction
Each server has its own protections and rules. Manage them in the tool server's **Security** tab.
* **Protections** are built-in checks that can find sensitive content.
* **Rules** are manual controls that you can add. They allow for more specific targeting.
## Protections
| Mode | Effect |
| --------- | -------------------------------------------------------------------- |
| **Off** | Skip scans, unless a protection rule applies to the tool |
| **Audit** | Record matches without changing tool arguments or responses |
| **On** | Apply the configured Request and Response actions and record matches |
The **Secrets**, **Personal data** and **Prompt injection** protections are available.
### Directions
A protection can scan two directions:
* **Request**: the arguments that the client sends. In the API, the name is `arguments`.
* **Response**: the result, upstream error messages and progress messages. In the API, the name is `result`.
Each direction has an action for **On** mode. Open the protection's mode menu, then select **Request** or **Response** under **Actions**.
| Action | Effect |
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| **Off** | Skip scans in this direction |
| **Audit** | Record matches and pass the content unchanged |
| **Redact** | Replace matches with a marker, such as `[REDACTED:github-pat]` |
| **Block** | Refuse a matching request before the tool runs, or withhold a matching result or upstream error message after the tool runs |
For progress messages, **Block** replaces matches with a marker. It does not stop the tool.
The built-in actions are:
| Protection | Request | Response |
| -------------------- | ----------- | ---------- |
| **Secrets** | **Block** | **Redact** |
| **Personal data** | **Audit** | **Redact** |
| **Prompt injection** | Not scanned | **Redact** |
In **Audit** mode, Gatana scans the same directions as **On** mode, but only records matches. A direction set to **Off** is not scanned in either mode unless a protection rule applies.
Select **Default (...)** to remove a server action and use the organization default or built-in value. The menu shows where the current value comes from. An asterisk (`*`) beside the mode shows that a server action or option differs from its default.
With Request **Off**, this protection does not scan the arguments. Audit and trace logs can contain them as sent. With Response **Off**, this protection passes results and messages unchanged. Protection rules can still enable scans for selected tools.
### Defaults for your organization
Each action and setting comes from the first level that sets it:
1. The server.
2. The organization default.
3. The built-in value.
A server without its own value follows the organization default, including later changes. A list replaces the list from the next level; the lists are not combined.
Users with permission to manage the organization set defaults on the **Protections & Firewall** page:
1. Select **Edit defaults**.
2. Change the values.
3. Select **Save**.
The defaults apply as follows:
* **Mode for new servers** sets the initial mode. Later changes do not change existing server modes. Without an organization default, a new server has Secrets **On** and the other protections **Off**.
* **Request** and **Response** apply to all servers without their own action, including existing servers.
* Settings set through the API also apply to all servers without their own value.
You can change modes for existing servers on the **Protections & Firewall** page. Select the servers, then use the bulk actions.
### API
Set `protections` in a server API request. For example:
```json
{
"protections": {
"secrets": {
"mode": "on",
"actions": {
"arguments": "block",
"result": "redact"
},
"settings": {
"includeGeneric": false
}
}
}
}
```
**Broad detection** (`includeGeneric`) also finds generic `key=value` secrets. It is off by default. Set it through the API, on a server or as an organization default.
**Phrase detection** (`phrases`) enables injection phrase rules. It is on by default. Set it to `false` to scan only for hidden characters. Change it in the protection's mode menu, through the API, or as an organization default.
Missing actions and settings use organization defaults or built-in values. Each server stores its own mode. A missing protection on an existing server is **Off**.
The read-only field `resolvedProtections` in server responses shows:
* Each direction's action for **On** mode, regardless of the current mode.
* Each setting's value.
* The `source` of each value: `server`, `organization` or `builtin`.
* The `inherited` value that applies if the server removes its own value.
The tenant field `protectionDefaults` stores organization defaults in the same format. An update to this field replaces all organization protection defaults. Omitted values use built-in defaults.
To remove a server action or setting, send the complete `protections` object without that key in a `PUT` request. Omitted keys in a `PATCH` request keep their current values.
### Personal data
The **Personal data** protection is off by default. It uses patterns and checksums, without a model or an external service. The rules accept missed values to reduce false matches, because redaction changes tool results.
| Type | When it matches |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Email address | Anywhere. It does not match service addresses such as `noreply@...`, or clone URLs such as `git@github.com:...` |
| Phone number | Anywhere, if it starts with `+` and is valid. Other forms require a label such as `phone` |
| IBAN | Anywhere, with a correct checksum |
| Card number | Anywhere, with a correct checksum and a known issuer. Numbers without separators are excluded from ID fields, such as `id` or `order_id` |
| National ID number | With a correct checksum. Formats that are easy to identify match anywhere. Other forms require a label such as `ssn` or `personnummer` |
| Name, birth date, street address, passport number | Only in a field that identifies the type, such as `first_name`, `date_of_birth` or `street_address`. These types do not match in sentences or fields named only `name` or `address` |
A field name can serve as the label for its value.
National ID numbers are supported for Australia, Austria, Belgium, Canada, Croatia, Czechia, Denmark, Estonia, Finland, France, Germany, Greece, Iceland, India, Ireland, Italy, the Netherlands, Norway, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden, Switzerland, the United Kingdom and the United States.
The redaction marker identifies the type, such as `[REDACTED:email-address]` or `[REDACTED:se-personnummer]`.
With Request **Audit**, the tool and the audit record receive the arguments unchanged, including personal data. Set Request to **Redact** to remove personal data before the arguments reach the tool and the audit record.
### Prompt injection
The **Prompt injection** protection is off by default. It scans tool responses for hidden characters and injection phrases. It uses patterns without a model or an external service.
| Group | What it finds | What **Redact** does |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Hidden characters | Unicode tag characters that carry invisible text, four or more zero-width characters in a row, also when spaces separate them, zero-width characters next to other hidden characters, a zero-width character inside a Latin word, bidirectional overrides (U+202D, U+202E) and other invisible characters | Deletes the characters. The visible text stays |
| Phrases | Sentences that override instructions, address an AI, assign the AI a new role, ask the AI to hide something from the user, chat template markers such as `<\|im_start\|>`, and tags such as `` | Replaces the whole text block, string value or message with a marker such as `[REDACTED:instruction-override]`, because the sentence around the phrase carries the instruction |
The phrase rules cover English and German. Hidden-character rules apply to all languages. Before the phrase scan, Gatana removes hidden characters, converts typographic apostrophes, no-break spaces and fullwidth letters to plain text, and joins wrapped lines. A blank line ends a sentence.
Emoji keep their joiners, and the flags of England, Scotland and Wales keep their tag characters. Bidirectional embeddings and isolates, which right-to-left text uses, are not changed.
The protection does not find instructions that read like a normal task, such as "translate this into French", requests that do not address an AI, encoded text such as base64, look-alike letters, phrases in other languages, text hidden with CSS, or image links that carry data. Use **Audit** first to see what it finds in your tool results.
With **Phrase detection** off, the protection finds hidden characters only. Use this on servers that return email threads, documents or web pages, where people also write "ignore my previous instructions".
The audit record names each matched rule, such as `unicode-tags` or `instruction-override`. It does not include the matched text.
See [gatana.ai/measurements](https://www.gatana.ai/measurements#injection-detection) for detection rates, false matches, the method and the data.
### What is scanned
Gatana scans the following content when the direction is enabled:
* string values in arguments and structured results
* result text
* text resources
* upstream error and progress messages.
If result text, a text resource or a message is valid JSON, Gatana decodes and scans each string separately. Its key serves as the label. Gatana also scans keys and numbers with 7 or more digits. Matched keys and numbers become string markers. Spacing and other values stay unchanged.
Keys and numbers in arguments and structured results are not scanned, because a marker can break the schema.
If JSON is only part of a text, Gatana scans the whole text as one part.
It does not scan images, audio, other binary content or resource links. It does not decode base64 or other encodings inside text.
Redaction can make a value fail schema validation. Prompt injection deletes hidden characters without a marker. This can avoid failures caused by markers. The changed value can still fail validation.
## Rules
A rule applies to **All tool calls** or **Specific tools**. It uses either a condition or a protection check.
### Condition rules
A condition rule uses these actions:
| Action | Effect |
| --------- | ------------------------------------------------------------------------------------- |
| **Deny** | Stop the call before the tool runs |
| **Audit** | Record the match and continue to the remaining rules and protections |
| **Allow** | Make protections record matches without blocking or redacting. Deny rules still apply |
Rules run in order from top to bottom. **Deny** stops evaluation. **Audit** and **Allow** continue, so a later **Deny** still blocks a call.
A missing argument makes a condition such as `args.confirm != true` not match. Other evaluation errors, such as comparing text with a number, block the call.
Use `args` in an **Allow** condition only for trusted callers. Callers control the arguments and can make the rule match to avoid blocking and redaction.
### Protection rules
A protection rule selects a protection and a direction, such as **Secret in request**. It uses **Block**, **Redact** or **Audit** with the effects described above.
For each protection and direction, the first rule that applies to the tool selects the action. It takes priority over the protection mode and direction action, even when either is **Off**. A matching **Allow** condition rule still changes the selected action to **Audit**.
## Configuration
1. Open a tool server under **Servers**, then select **Security**.
2. Open each protection's mode menu to set its mode and Request and Response actions.
3. Select **Add rule**, choose the tools, condition or protection check, and action, then **Save**. For conditions, use the expression builder or the **CEL** tab.
4. Use each rule's menu to move, duplicate or delete it.
Changes are saved immediately and take effect within 30 seconds.
**Test a call** checks rules and argument protections without running the tool or writing audit records.
## Condition fields
| Field | Value |
| ------------------------------------------------- | -------------------------------------- |
| `tool`, `args.*` | Tool name and arguments |
| `user.id`, `user.email`, `user.name`, `user.role` | Caller details |
| `user.isServiceAccount`, `user.isDisabled` | Caller flags |
| `teams`, `profiles` | Caller team IDs and active profile IDs |
| `claims.*`, `authMethod` | Token claims and authentication method |
For example, deny writes outside a directory:
```js
tool == "write_file" && !args.path.startsWith("/workspace/")
```
## Auditing
See the server's [Audit Logs](/audit-logs) tab or use [SIEM Streaming](/siem-streaming). Deny and Audit condition rules write `firewall_deny` and `firewall_log` events.
Protection matches appear in `protectionChecks`, without matched text. For **Allow**, `wouldHaveAction` shows the action replaced by **Audit**. Older records use the action `log` for **Audit**.
# Fundamentals
Share tools in your gateway by adding tool servers
## Introduction
Organizations give their people and their agents access to tools: issue trackers, code hosts, documentation, internal APIs. Each tool lives behind its own server, with its own protocol, its own credentials and its own way to run. A tool server in Gatana is the object that describes one such resource. When you add a tool server to your gateway, its tools become available to the people, teams and agents you give access to, through one MCP endpoint.
## Definitions
* **MCP**: the [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro). The open protocol that AI clients such as Claude, Cursor and GitHub Copilot use to list and call tools.
* **Tool server**: provides tools in your gateway. It configuration describes how it runs, who may use it and which tools it exposes. The rest of the documentation calls it a *server*.
* **Transport**: the way Gatana talks to a server: a remote HTTP endpoint, an executable file Gatana starts, FaaS code Gatana runs, an OpenAPI specification or a built-in implementation.
* **Tool**: one function a server offers. Gatana caches the tool list of each server and exposes every tool to clients as `{slug}_{toolname}`.
* **Slug**: the short technical identifier of a server (`a-z` and hyphens). It is unique in the organization and it is prepended to each tool name.
* **Credential**: the resolved secret that Gatana uses when it calls a tool: an API key or an OAuth token. See [Credentials](/credentials).
## What is a tool server?
A tool server is the unit you manage in the Gatana dashboard, the CLI and the [public API](/public-api-introduction). The server is the source of truth for everything the gateway needs in order to call its tools. Gatana discovers the tools, stores them, applies your overrides, checks who is calling, resolves the credentials, evaluates the firewall rules and returns the result to the client.
Whatever a server runs on, your AI clients see the same thing: one remote MCP endpoint that lists the tools of every server they have access to. Gatana supports these kinds of servers:
| Kind | Where it runs | Documentation |
| ------------------ | ----------------------------------------------------------------------------- | -------------------------------------------------------- |
| **Remote HTTP** | Outside Gatana, at a URL you provide (Streamable HTTP or SSE) | [Standard MCP](/servers-standard-mcp#remote-http) |
| **Executable App** | In a container that Gatana starts from a command and a Docker image | [Standard MCP](/servers-standard-mcp#executable-apps) |
| **Runnable Code** | In a runtime that Gatana hosts (Node.js or Python), from source code you push | [Runnable Code](/servers-runnable-code) |
| **OpenAPI** | Outside Gatana. Gatana generates one tool per operation in the specification | [OpenAPI / Swagger](/servers-openapi-swagger) |
| **Built-in** | Inside Gatana, nothing to host | [Adding a Server](/gateway-introduction#adding-a-server) |
The kind only changes how Gatana talks to that one server. Everything else on this page applies to all of them.
## Why use tool servers?
Without a gateway, each person connects each AI client to each MCP server by hand. API keys end up in configuration files on laptops, nobody can say which keys exist, and there is no record of what the agents did with them. Tool servers in Gatana replace that with one connection per client and central control over the rest:
* **One endpoint.** A client connects to the gateway once and gets every tool the user is allowed to use.
* **Central credentials.** Keys and OAuth tokens are stored in Gatana, shared for the whole server or held per user. They never reach the client.
* **Access control.** Membership and roles decide who can see a server and who can call its tools.
* **Tool shaping.** Disable tools, rename them, rewrite their descriptions and input schemas, so the model gets a tool list that fits the task.
* **Guard rails.** [Firewall rules](/tool-firewall) deny or log calls, [compression](/compression) keeps large outputs out of the context window, and every call is written to the [audit log](/audit-logs).
Building this yourself means a proxy, a secret store, an OAuth client, a policy engine and a log pipeline. Gatana handles that work so you only describe the server.
## What your users experience
Consider a team that wants its agents to work with GitHub.
### Your team does not use Gatana
1. Each developer finds the GitHub MCP server and installs it on their machine.
2. Each developer creates a personal access token on GitHub and pastes it into the configuration file of their AI client.
3. They repeat step 2 for every AI client they use.
4. A developer who wants a safer tool list removes tools by editing the configuration, if the client allows it.
5. When a developer leaves, the tokens stay valid until somebody remembers to revoke them.
*The tokens are spread across machines and files. No one can list them, rotate them or see what they were used for.*
### Your team uses Gatana
1. A maintainer adds the GitHub server to the gateway once and grants the team access.
2. Each developer connects their AI client to the gateway, signs in and sees the GitHub tools. **Optional.** When the server uses per-user credentials, the developer authorizes GitHub once in Gatana.
3. The maintainer disables the destructive tools and adds a firewall rule for the rest. The change applies to every client at once.
When a developer leaves the organization, their access to every server ends with their account.
## Overview of the parts
A server is made of a few parts. Each one has its own page where it needs more explanation.
### Server
The server record holds the settings that apply to the server as a whole:
| Setting | What it does |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Slug** and **description** | Identify the server for people and for the model. Keep the slug short: tool names commonly have a limit of 64 characters. |
| **Visibility** | `private` shows the server only to its members, `organization` shows it to everyone. Seeing a server is not the same as using it. See [Permissions and Roles](/permissions-and-roles#how-access-is-decided). |
| **Enabled** | A disabled server keeps its configuration but exposes no tools. |
| **Timeouts** | A limit for a single protocol request, and a total limit for a tool call. A progress notification from the server can reset the protocol timeout. |
| **Compression** | Whether large tool outputs are compressed, and the size at which compression starts. See [Compression](/compression). |
| **Firewall rules** | The ordered list of rules evaluated against each tool call. See [Firewall](/tool-firewall). |
### Transport
The transport is how Gatana reaches the server. It is fixed by the kind of server you chose when you added it: a URL and headers for a remote server, a command, image and environment for an executable app, a runtime for runnable code, a specification and base URL for OpenAPI. Servers that Gatana runs can also reserve CPU and memory, keep a [persistent volume](/persistent-storage) and join your [Tailscale](/tailscale) network.
### Authorization and credentials
The authorization configuration describes what form the credentials have: none, API keys, OAuth 2.1 or OAuth client credentials. It also sets whether one credential is shared by the whole server or each user provides their own. The credentials themselves are stored separately and can be scoped to the server, to a user or to a [profile](/profiles). See [Credentials](/credentials).
### Tools
Gatana discovers the tools of a server and caches them. The cache is refreshed automatically every couple of hours, or on demand with the **Refresh** button on the server page. Each cached tool keeps the name, description, input schema and annotations the server reported, plus your overrides: a tool can be disabled, renamed, given a new description or a new input schema. The client only sees the result.
### Members
Access to a server is granted to users and teams. Each membership has a role:
* **Member**: sees and calls the tools.
* **Maintainer**: changes the server configuration.
* **Admin**: manages members and deletes the server.
Organization-wide base permissions can grant a role to everyone without a membership. See [Permissions and Roles](/permissions-and-roles).
### Files
An [executable app](/servers-standard-mcp#executable-apps) can be given files. You upload them in Gatana, or point at a value in AWS Systems Manager or Secrets Manager, and they appear in the folder the app runs in, under the file name you choose. Use this for service account keys and other material that does not fit in an environment variable. Secrets that go into headers, environment variables or the command line come from [secret stores](/secret-stores) instead.
Add your first server from the [Adding a Server](/gateway-introduction#adding-a-server) section, then set up its [credentials](/credentials).
# Zero-Touch Onboarding
With trusted identity providers your users gain seamless access to Gatana
## The Hidden Problem
You've set up MCP servers for your team. Now what?
Most teams end up with one of two bad options:
1. **Shared credentials** — Everyone uses the same API key.
2. **Manual provisioning** — IT manually grants access to each user.
There's a better way: **let your identity provider handle it**.
## Gatana's Approach
Gatana implements zero trust principles access through two mechanisms:
* **OIDC Token Trust** — Accept access tokens directly from your corporate IdP
* **Claim-Based Mapping** — Automatically apply the right permissions based on token claims
The result: a new employee authenticates once, and Gatana knows exactly which tools they can access and which credentials to inject—based entirely on who they are in your IdP.
## How to Enable
### 1. Enable OIDC Token Trust
Configure Gatana to accept access tokens from your identity provider:
1. Go to **Settings** → **Authentication**
2. Add your OIDC provider (Okta, Microsoft Entra, Auth0, etc.)
3. Enable **OIDC Access Token Trust**
Any valid access token from your IdP with the Client ID from step (2) can now authenticate to Gatana. Users are automatically provisioned on first access—no manual setup required.
See [Identity Federation](/federation) for detailed provider setup instructions.
### 2. Create Profiles with Credential Overrides
Profiles let you inject team-specific credentials for tools that don't support per-user OAuth:
1. Create a profile (e.g., `platform-team`)
2. Add the relevant MCP servers
3. Configure credential overrides with team API keys
4. Disable **Open to All** to restrict access
### 3. Map Claims to Profiles
The key to zero-touch access: map IdP claims to profiles.
In the profile settings, add claim mappings:
| Claim Key | Claim Value |
| --------- | ----------- |
| `team` | `platform` |
Now anyone whose token contains `team: platform` automatically gets the `platform-team` profile applied—with all its servers and credentials.
**Day-one experience:**
1. New hire is added to `platform` team in your IdP
2. They open their AI assistant
3. Gatana validates their token, sees the `team: platform` claim
4. They immediately have access to the right tools with the right credentials.
## Related Documentation
* [Identity Federation](/federation) — Configure OIDC/SAML providers
* [Profiles](/profiles) — Credential overrides and claim mappings
* [Credentials](/credentials) — How credential scoping works
* [Agent-2-Agent](/agent-2-agent) — How Gatana enables Agent-2-Agent architectures
# LangChain Deep Agents
Use Gatana Sandboxes with Langchain Deep Agent
The Agentic Sandbox is currently in **early access**. Features and APIs may change. To enable it for your organization, you can opt-in in the Sandbox page in the Gatana Dashboard.
Gatana provides a Python implementation of the LangChain Deep Agent sandbox backend.
```
$ uv add gatana-langchain
```
```python
from gatana_client import GatanaClient
from langchain_anthropic import ChatAnthropic
from deepagents import create_deep_agent
from gatana_langchain import GatanaSandbox
# Env variables: GATANA_API_KEY and GATANA_ORG_ID
# Or, ~/.gatana.config
client = GatanaClient()
with GatanaSandbox(client=client) as backend:
agent = create_deep_agent(
model=ChatAnthropic(model="claude-sonnet-4-20250514"),
system_prompt="You are a coding assistant with sandbox access.",
backend=backend,
)
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "Create a Python script that prints the Fibonacci sequence and run it",
}
]
}
)
print(result["messages"][-1].content)
# Sandbox is automatically deleted when the `with` block exits.
```
For more details, see:
* [Gatana Python GitHub repository](https://github.com/gatana-ai/gatana-python/tree/main/packages/gatana-langchain)
* [Sandbox Documentation](/sandbox-introduction)
# LangChain MCP
Integrate with Gatana MCP Gateway for Langchain Agents
Below is a simple example on how to use Gatana MCP Gateway with [LangChain](https://www.langchain.com/):
```python
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.sessions import StreamableHttpConnection
from langchain.agents import create_agent
client = MultiServerMCPClient(
connections={
"gatana": StreamableHttpConnection(
transport="streamable_http",
url="https://ORG_ID.gatana.ai/mcp",
headers={"Authorization": f"Bearer {access_token}"},
)
}
)
tools = await client.get_tools()
agent = create_agent(model, tools=tools)
result = await agent.ainvoke({"messages": [HumanMessage(content="Hello")]})
```
# OpenRouter
[OpenRouter](https://openrouter.ai/) uses OpenAI tool definitions, so in addition to retrieving tools from Gatana, the definitions must also be converted from MCP to OpenAI format.
### Example Usage
```python
"""
OpenRouter + Gatana Integration Example
A minimal example showing how to use Gatana MCP servers with OpenRouter.
Requirements:
pip install mcp openai python-dotenv
Environment variables (.env):
OPENAI_API_KEY=sk-or-v1-your-openrouter-key
GATANA_ORG_ID=your-org-id
GATANA_TOKEN=your-gatana-token
"""
import asyncio
import os
from dotenv import load_dotenv
from gatana import GatanaClient
load_dotenv()
# Configuration
ORG_ID = os.getenv("GATANA_ORG_ID")
TOKEN = os.getenv("GATANA_TOKEN") # Or exchange from your federated IDP or directly with Gatana
MODEL = "anthropic/claude-4.5-opus"
if not ORG_ID or not TOKEN:
print("Error: Please set GATANA_ORG_ID and GATANA_TOKEN in your .env file")
print("Copy .env.example to .env and fill in your values")
exit(1)
if not os.getenv("OPENAI_API_KEY"):
print("Error: Please set OPENAI_API_KEY (OpenRouter key) in your .env file")
exit(1)
async def main():
print("OpenRouter + Gatana Demo")
print("-" * 40)
client = GatanaClient(
org_id=ORG_ID,
token=TOKEN,
model=MODEL
)
async with client:
# Show available tools
tools = await client.list_tools()
print(f"Available tools: {tools}\n")
while True:
query = input("\nYou: ").strip()
if query.lower() in ['quit', 'exit', 'q']:
break
if query:
response = await client.chat(query)
print(f"\nAssistant: {response}")
if __name__ == "__main__":
asyncio.run(main())
```
### GatanaClient
In `gatana/` add this file:
```python
"""
Gatana Client - Bridge MCP servers with OpenRouter.
"""
import json
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from openai import OpenAI
def _convert_tool_format(tool):
"""Convert MCP tool definitions to OpenAI-compatible format."""
return {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": {
"type": "object",
"properties": tool.inputSchema.get("properties", {}),
"required": tool.inputSchema.get("required", [])
}
}
}
class GatanaClient:
"""
A client that bridges Gatana MCP servers with OpenRouter.
This client:
1. Connects to a Gatana MCP server via Streamable HTTP
2. Converts MCP tools to OpenAI-compatible format
3. Uses OpenRouter to get LLM responses with tool calls
4. Executes tool calls via the MCP server
Example:
client = GatanaClient(
org_id="your-org-id",
token="your-gatana-token",
openrouter_api_key="sk-or-..."
)
async with client:
response = await client.chat("Hello!")
print(response)
"""
def __init__(
self,
org_id: str,
token: str,
openrouter_api_key: Optional[str] = None,
model: str = "anthropic/claude-3.5-sonnet",
openrouter_base_url: str = "https://openrouter.ai/api/v1"
):
"""
Initialize the Gatana client.
Args:
org_id: Your Gatana organization ID
token: Gatana authentication token
openrouter_api_key: OpenRouter API key (or set OPENAI_API_KEY env var)
model: Model to use via OpenRouter (default: anthropic/claude-3.5-sonnet)
openrouter_base_url: OpenRouter API base URL
"""
self.org_id = org_id
self.mcp_server_url = f"https://{org_id}.gatana.ai/mcp"
self.model = model
# Build MCP headers
self.mcp_headers = {"Authorization": f"Bearer {token}"}
# Initialize OpenAI client for OpenRouter
self.openai = OpenAI(
api_key=openrouter_api_key,
base_url=openrouter_base_url
)
# Session management
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.messages = []
self._connected = False
async def connect(self):
"""Connect to the MCP server."""
if self._connected:
return
http_transport = await self.exit_stack.enter_async_context(
streamablehttp_client(self.mcp_server_url, headers=self.mcp_headers)
)
read_stream, write_stream, _ = http_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(read_stream, write_stream)
)
await self.session.initialize()
self._connected = True
async def disconnect(self):
"""Disconnect from the MCP server."""
if self._connected:
await self.exit_stack.aclose()
self._connected = False
async def __aenter__(self):
"""Async context manager entry."""
await self.connect()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit."""
await self.disconnect()
async def list_tools(self) -> list:
"""List available tools from the MCP server."""
if not self._connected:
raise RuntimeError("Not connected. Call connect() or use async with.")
response = await self.session.list_tools()
return [tool.name for tool in response.tools]
async def chat(
self,
message: str,
max_tokens: int = 1000,
reset_history: bool = False
) -> str:
"""
Send a message and get a response, with automatic tool execution.
Args:
message: The user message to send
max_tokens: Maximum tokens in the response
reset_history: If True, clear conversation history before this message
Returns:
The assistant's response text
"""
if not self._connected:
raise RuntimeError("Not connected. Call connect() or use async with.")
if reset_history:
self.messages = []
self.messages.append({
"role": "user",
"content": message
})
# Get available tools
response = await self.session.list_tools()
available_tools = [_convert_tool_format(tool) for tool in response.tools]
# Call OpenRouter
response = self.openai.chat.completions.create(
model=self.model,
tools=available_tools if available_tools else None,
messages=self.messages,
max_tokens=max_tokens
)
self.messages.append(response.choices[0].message.model_dump())
final_text = []
content = response.choices[0].message
if content.tool_calls:
# Process all tool calls
for tool_call in content.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}
try:
result = await self.session.call_tool(tool_name, tool_args)
# Format result content
result_content = result.content
if isinstance(result_content, list):
result_content = json.dumps([
item.model_dump() if hasattr(item, 'model_dump') else str(item)
for item in result_content
])
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_name,
"content": str(result_content)
})
except Exception as e:
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_name,
"content": f"Error: {str(e)}"
})
# Get final response after tool execution
response = self.openai.chat.completions.create(
model=self.model,
max_tokens=max_tokens,
messages=self.messages,
)
final_text.append(response.choices[0].message.content)
else:
final_text.append(content.content)
return "\n".join(final_text)
def clear_history(self):
"""Clear the conversation history."""
self.messages = []
```
# Google Workspace MCP
See this reference configurations to get more familiar with how to use Gatana
The `workspace-mcp` is a powerful MCP server which has a somewhat complex configuration requirement. It is fully supported in Gatana, and below is a step-by-step guide on how to configure it for full access to Gmail.
This version was made targeting [workspace-mcp v1.5.4](https://github.com/taylorwilsdon/google_workspace_mcp/releases/tag/v1.5.4). Newer version might require different settings.
First, we need to configure an OAuth client.
1. Navigate to [https://console.cloud.google.com/auth/clients](https://console.cloud.google.com/auth/clients)
2. Create a new client of type **Web Application** with redirect uri `https://gatana.ai/api/v1/mcp-servers/oauth/callback`
3. Remember the client ID and secret (Google will only show you the secret *once*)
4. Grant the scopes `https://mail.google.com/ openid .../auth/userinfo.email .../auth/userinfo.profile`
Now, we are ready to create our server in Gatana.
1. Go to Gatana App, navigate to **Servers**
2. In the top-right corner, click **Add Server**
3. Enter a name, e.g. **Google Mail** and select **Transport Type**: **Local**, and save the server
4. After server creation, you will be redirected to the settings page of the server
5. Configure:
* **Command**: `uvx workspace-mcp --transport streamable-http --tools gmail`
* **Transport**: Streaming HTTP
* **Listening Port**: 8080
6. Provide the environment variables:
* **MCP\_ENABLE\_OAUTH21** = true
* **EXTERNAL\_OAUTH21\_PROVIDER** = true
* **WORKSPACE\_MCP\_STATELESS\_MODE** = true
* **MCP\_REMOVE\_USER\_GOOGLE\_EMAIL\_ARG** = true
* **WORKSPACE\_MCP\_PORT** = 8080
* **GOOGLE\_OAUTH\_CLIENT\_ID** = Your Client ID we created above
* **GOOGLE\_OAUTH\_CLIENT\_SECRET** = Some non-empty string, e.g. "HELLO123" (its precense needed by workspace-mcp, but the value is never checked)
7. Under Authorization, set
* **Credentials Scope: Server**
* **Method: OAuth**.
8. Under **OAuth Issuer** enter **Issuer**: [https://accounts.google.com](https://accounts.google.com), then click on **Discover**. This will automatically fill out the issuer configuration.
9. Set **Extra Parameters for Authorization Endpoint**: `consent=prompt&access_type=offline`. This a proprietary requirement from the Google OAuth implementation for getting an access token which is long-lived
10. Under **OAuth Client**, fill out the Client ID and Secret from the one you created above.
11. Set
* **Scopes**: `https://mail.google.com/ openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile`
* **Client Authentication Method**: POST Body Parameters (Google *does not* support Basic Authentication)
* **Grant**: Authorization Code
If you want each Gatana user to have their own set of credentials for this server, go into **Settings** and set the **Credentials Scope** to **User**
Now, you can save the server. You will be redirected to the detail pages of the server. There is one final step left: providing the default credentials.
1. In the top-right corner, click on **Credentials**
2. Click on **Authorize Server**
3. This will redirect you to Google to authorize this MCP Server
Complete the Google authorization flow, and finally, the server is ready. In the **Tools** section,
click on **Refresh** to retrieve the available tools.
You can test the connectivity by clicking on the magnify button for a tool. An example is **list\_gmail\_labels** which does not require any arguments. It should return something similar:
```json
{
"result": {
"content": [
{
"type": "text",
"text": "Found 14 labels:\n\n📂 SYSTEM LABELS:\n • CHAT (ID: CHAT)\n • SENT (ID: SENT)\n • INBOX (ID: INBOX)\n • IMPORTANT (ID: IMPORTANT)\n • TRASH (ID: TRASH)\n • DRAFT (ID: DRAFT)\n • SPAM (ID: SPAM)\n • CATEGORY_FORUMS (ID: CATEGORY_FORUMS)\n • CATEGORY_UPDATES (ID: CATEGORY_UPDATES)\n • CATEGORY_PERSONAL (ID: CATEGORY_PERSONAL)\n • CATEGORY_PROMOTIONS (ID: CATEGORY_PROMOTIONS)\n • CATEGORY_SOCIAL (ID: CATEGORY_SOCIAL)\n • STARRED (ID: STARRED)\n • UNREAD (ID: UNREAD)\n"
}
],
"structuredContent": {
"result": "Found 14 labels:\n\n📂 SYSTEM LABELS:\n • CHAT (ID: CHAT)\n • SENT (ID: SENT)\n • INBOX (ID: INBOX)\n • IMPORTANT (ID: IMPORTANT)\n • TRASH (ID: TRASH)\n • DRAFT (ID: DRAFT)\n • SPAM (ID: SPAM)\n • CATEGORY_FORUMS (ID: CATEGORY_FORUMS)\n • CATEGORY_UPDATES (ID: CATEGORY_UPDATES)\n • CATEGORY_PERSONAL (ID: CATEGORY_PERSONAL)\n • CATEGORY_PROMOTIONS (ID: CATEGORY_PROMOTIONS)\n • CATEGORY_SOCIAL (ID: CATEGORY_SOCIAL)\n • STARRED (ID: STARRED)\n • UNREAD (ID: UNREAD)\n"
},
"isError": false
}
}
```
# /artifact-themes
{/* Auto-generated */}
List the artifact themes of the organization and the theme new artifacts get by default
# /artifact-themes
{/* Auto-generated */}
Create an organization theme for artifacts: the Gatana theme with the organization's colors, fonts and logo
# /audit-logs
{/* Auto-generated */}
List audit log summaries page by page, limited to entries for the caller unless full access is granted; get one entry for complete details
# /connected-clients
{/* Auto-generated */}
List the OAuth clients the signed-in user has authorized
# /artifacts
{/* Auto-generated */}
List the artifacts the caller can read: their own, the ones shared with them or one of their teams, and the ones shared with the organization
# /artifacts
{/* Auto-generated */}
Create an artifact from a Markdown or HTML document and return its viewer URL
# /mcp-server-usage
{/* Auto-generated */}
Count the MCP requests made to each server the caller has access to over the last seven days
# /profiles
{/* Auto-generated */}
List the profiles the caller can read, optionally filtered to one MCP server
# /profiles
{/* Auto-generated */}
Create a new profile without any MCP servers attached
# /sandboxes
{/* Auto-generated */}
List the sandboxes the caller can read. Set all=true to also include archived sandboxes
# /sandboxes
{/* Auto-generated */}
Create a sandbox for the authenticated user with a paid subscription and Kubernetes, up to 10 concurrent sandboxes per organization
# /mcp-servers
{/* Auto-generated */}
List all MCP servers the caller has access to
# /mcp-servers
{/* Auto-generated */}
Create a new MCP server
# /secret-stores
{/* Auto-generated */}
List all secret stores for the tenant
# /secret-stores
{/* Auto-generated */}
Create a new secret store
# /siem-destination
{/* Auto-generated */}
Remove the SIEM streaming destination
# /siem-destination
{/* Auto-generated */}
Get the SIEM streaming destination of the organization, or null when none is configured
# /siem-destination
{/* Auto-generated */}
Update the SIEM streaming destination
# /siem-destination
{/* Auto-generated */}
Configure SIEM streaming. The signing secret is returned once and cannot be retrieved later
# /skill-collections
{/* Auto-generated */}
List the skill collections the caller can see, each with how many of its skills the caller can read
# /skill-collections
{/* Auto-generated */}
Create a skill collection from a name, an optional one-line description and a general access
# /skills
{/* Auto-generated */}
List the skills the caller can read and the collections they can see, optionally narrowed to one collection or to a text in the name or description
# /skills
{/* Auto-generated */}
Create a skill from a SKILL.md document, or from a name, a one-line description of when to use it and Markdown instructions, at root or in a collection
# /teams
{/* Auto-generated */}
List the teams the caller can read, optionally only those a given user is a member of
# /teams
{/* Auto-generated */}
Create a new team. The name must be unique in the organization
# /users
{/* Auto-generated */}
List the users and service accounts the caller can read, optionally filtered by type
# /users
{/* Auto-generated */}
Create a service account or invite a user by email, returning the invitation link only when email is disabled
# /tools
{/* Auto-generated */}
List all cached tools of every MCP server the caller has access to, including the disabled ones
# /credentials
{/* Auto-generated */}
List the credentials the caller can read, optionally filtered by server. Secret values are not included
# /servers
{/* Auto-generated */}
List all MCP servers the caller has access to
# /servers
{/* Auto-generated */}
Create a new MCP server. Requires `slug` and `transportConfig`. All other fields are optional with sensible defaults. Read-only fields are silently ignored
# /artifact-themes/default
{/* Auto-generated */}
Set the theme new artifacts get when their creator names none; existing artifacts keep their theme
# /artifact-themes/preview
{/* Auto-generated */}
Render a sample Markdown document with a theme that is not saved yet, starting from the Gatana values for any field left out
# /artifact-themes/{themeId}
{/* Auto-generated */}
Delete an organization theme; the artifacts on it, and the organization default when it names it, go back to Gatana
# /artifact-themes/{themeId}
{/* Auto-generated */}
Get an organization theme for artifacts with its logo and extra CSS
# /artifact-themes/{themeId}
{/* Auto-generated */}
Change an organization theme; every artifact on it shows the change the next time it is opened
# /audit-logs/{id}
{/* Auto-generated */}
Get a single audit log entry. Callers without full access can only read their own entries
# /audit-logs/filter-options
{/* Auto-generated */}
List the values a caller may filter the audit log by, for each field that has a known set
# /audit-logs/summary
{/* Auto-generated */}
Summarize the tool calls the caller made over a recent window, as a count per day plus the servers they went to
# /connected-clients/{clientId}
{/* Auto-generated */}
Disconnect a client, revoking its grants and every token issued under them
# /connected-clients/{clientId}
{/* Auto-generated */}
Update the name or the attached profiles of a connected client
# /deployments/deployment-logs
{/* Auto-generated */}
Stream the deployment and pod events of an MCP server as server-sent events. Requires read permission on the server and the Accept header text/event-stream; hostedFunctionId is no longer supported
# /deployments/metrics
{/* Auto-generated */}
Get historical CPU, memory and storage usage metrics for a deployment. Provide either serverSlug or sandboxId; requires read permission on the server
# /deployments/logs
{/* Auto-generated */}
Get the logs of a container of a pod, or stream them when the Accept header is text/event-stream. Defaults to the server container; set previous=true for the logs of the previous container instance
# /auth/me
{/* Auto-generated */}
Get metadata about the authenticated user
# /deployments/status
{/* Auto-generated */}
Get the deployment status of an MCP server or a sandbox. Provide either serverSlug or sandboxId; requires read permission on the server
# /artifacts/{artifactId}
{/* Auto-generated */}
Delete an artifact with all of its versions and stored content
# /artifacts/{artifactId}
{/* Auto-generated */}
Get an artifact with its version history and a frame URL for the requested or current version, honoring its visibility for anonymous callers
# /artifacts/{artifactId}
{/* Auto-generated */}
Change the title, visibility or theme of an artifact without touching its content
# /artifacts/{artifactId}
{/* Auto-generated */}
Update an artifact in place, storing new Markdown or HTML as a new version behind the same URL, or change its title or theme
# /email-verification/send-code
{/* Auto-generated */}
Send an email verification code
# /install-predefined/{id}
{/* Auto-generated */}
Install a predefined built-in MCP server for the current tenant and refresh its tool cache. Fails if the server is already installed. Requires manage permission on the tenant
# /profiles/{profileId}
{/* Auto-generated */}
Delete a profile. Only callers who can manage the profile are permitted
# /profiles/{profileId}
{/* Auto-generated */}
Get one profile with its MCP servers and server credentials
# /profiles/{profileId}
{/* Auto-generated */}
Update a profile. Closing a profile that was open to all users also removes it from all personal access tokens
# /sandboxes/{sandboxId}
{/* Auto-generated */}
Destroy the sandbox, delete its Kubernetes resources, and archive it
# /sandboxes/{sandboxId}
{/* Auto-generated */}
Get a single sandbox by ID
# /mcp-servers/access-preview
{/* Auto-generated */}
Preview which servers a specific user or team can access
# /mcp-servers/{serverSlug}
{/* Auto-generated */}
Delete an MCP server
# /mcp-servers/{serverSlug}
{/* Auto-generated */}
Get a single MCP server together with its files
# /mcp-servers/{serverSlug}
{/* Auto-generated */}
Update an MCP server. The slug can be changed, which also changes the URL of the server
# /scim-config/tokens
{/* Auto-generated */}
List the SCIM tokens of the organization. Requires permission to manage the organization
# /scim-config/tokens
{/* Auto-generated */}
Create a SCIM token for identity provider provisioning. The raw token is returned in the response. Requires permission to manage the organization
# /secret-stores/{id}
{/* Auto-generated */}
Delete a secret store
# /secret-stores/{id}
{/* Auto-generated */}
Get a secret store by ID
# /secret-stores/{id}
{/* Auto-generated */}
Update a secret store
# /siem-destination/rotate-secret
{/* Auto-generated */}
Generate a new signing secret. Batches are signed with it immediately
# /siem-destination/secret
{/* Auto-generated */}
Reveal the signing secret used to sign delivered batches
# /siem-destination/test
{/* Auto-generated */}
Send a single test event to the destination and report what it answered
# /skill-collections/{collectionId}
{/* Auto-generated */}
Delete a skill collection and move the skills in it to root
# /skill-collections/{collectionId}
{/* Auto-generated */}
Get a skill collection
# /skill-collections/{collectionId}
{/* Auto-generated */}
Rename a skill collection, or change its description or general access
# /skills/{skillId}
{/* Auto-generated */}
Delete a skill and its shares
# /skills/{skillId}
{/* Auto-generated */}
Get a skill with its Markdown instructions, also rendered as one SKILL.md document
# /skills/{skillId}
{/* Auto-generated */}
Update a skill from a SKILL.md document, or change its name, description, instructions, visibility or collection
# /teams/{teamId}
{/* Auto-generated */}
Delete a team
# /teams/{teamId}
{/* Auto-generated */}
Get a single team
# /teams/{teamId}
{/* Auto-generated */}
Update a team
# /tenants/{tenantId}
{/* Auto-generated */}
Get tenant details
# /users/me
{/* Auto-generated */}
Get the current authenticated user
# /users/me
{/* Auto-generated */}
Update the current user; require a browser session for password or email changes and a login within 12 hours except for a first password with email disabled and no SSO identity
# /users/{userId}
{/* Auto-generated */}
Delete a user permanently
# /users/{userId}
{/* Auto-generated */}
Get one user by ID
# /users/{userId}
{/* Auto-generated */}
Update a user; callers who are not the organization owner can change only the name
# /tools/search
{/* Auto-generated */}
Search the cached tools of every MCP server the caller has access to, page by page, including the disabled ones. Returns a list of tools paginated.
# /tools/refresh
{/* Auto-generated */}
Refresh the tool cache of one MCP server, or of all enabled servers when serverSlug is omitted. Streams the progress as server-sent events, so the Accept header must be text/event-stream
# /credentials/{id}
{/* Auto-generated */}
Delete a credential. Requires the delete permission on the credential
# /credentials/{id}
{/* Auto-generated */}
Get a credential by ID. Secret values are not included
# /servers/{slug}
{/* Auto-generated */}
Delete an MCP server
# /servers/{slug}
{/* Auto-generated */}
Get a single MCP server. The response body can be PUT back to update the server
# /servers/{slug}
{/* Auto-generated */}
Partially update an MCP server using JSON Merge Patch (RFC 7396). Send only the fields to change. Explicit `null` clears nullable fields (oauthMetadata, oauthClientConfiguration). Absent fields are untouched. Read-only fields are silently ignored
# /servers/{slug}
{/* Auto-generated */}
Update an MCP server. Accepts the full server object from GET; read-only fields (id, tenantId, createdAt, updatedAt, lastToolRefreshAt) are silently ignored. Supports updating isEnabled, authorization, and config in a single call
# /artifacts/{artifactId}/content
{/* Auto-generated */}
Get the served HTML of an artifact version as JSON, rendered and themed, plus the Markdown source when there is one
# /artifacts/{artifactId}/shares
{/* Auto-generated */}
List the users and teams an artifact is shared with
# /profiles/{profileId}/claim-mappings
{/* Auto-generated */}
List the identity claim mappings that give access to the profile
# /profiles/{profileId}/claim-mappings
{/* Auto-generated */}
Add an identity claim mapping that gives all users with the matching claim access to the profile
# /profiles/{profileId}/direct-assignments
{/* Auto-generated */}
List the accounts and teams that are assigned directly to the profile
# /profiles/{profileId}/maintainers
{/* Auto-generated */}
List the users who maintain the profile
# /profiles/{profileId}/maintainers
{/* Auto-generated */}
Add a user as a maintainer of the profile. Only callers who can manage the profile are permitted
# /sandboxes/{sandboxId}/audit-logs
{/* Auto-generated */}
List the audit log entries of the sandbox, paginated and optionally filtered by date range
# /profiles/{profileId}/tools
{/* Auto-generated */}
List the tool configuration of the profile for each of its MCP servers
# /sandboxes/{sandboxId}/exec
{/* Auto-generated */}
Run a command in the sandbox and stream the output back as NDJSON. The command and its result are written to the sandbox audit log
# /sandboxes/{sandboxId}/read-file
{/* Auto-generated */}
Read the file at the given path in the sandbox and stream its content back
# /sandboxes/{sandboxId}/ssh-session
{/* Auto-generated */}
Issue a short-lived token that opens an SSH session to the sandbox through the SSH proxy. The token expires after 5 minutes
# /sandboxes/{sandboxId}/write-file
{/* Auto-generated */}
Write the raw request body to the given file path in the sandbox
# /mcp-servers/{serverSlug}/all-tools-isEnabled
{/* Auto-generated */}
Enable or disable all tools of an MCP server. Tells the connected MCP clients that the tool list changed
# /mcp-servers/{serverSlug}/files
{/* Auto-generated */}
List the files of an MCP server. Only usable if the server has transport type "stdio" or "hosted"
# /mcp-servers/{serverSlug}/files
{/* Auto-generated */}
Upload a new file for an MCP server. Only usable if the server has transport type "stdio" or "hosted"
# /mcp-servers/{serverSlug}/isEnabled
{/* Auto-generated */}
Enable or disable an MCP server. Enabling fails if the organization has no subscription which permits more enabled servers
# /mcp-servers/{serverSlug}/members/
{/* Auto-generated */}
List the users and teams which are members of an MCP server, with their roles
# /mcp-servers/{serverSlug}/running-status
{/* Auto-generated */}
Get the running status of an MCP server. Servers with transport type "stdio" or "hosted" report the status of their containers. All other servers always report as running
# /mcp-servers/{serverSlug}/stop
{/* Auto-generated */}
Stops a running MCP server. Only usable if the server has transport type "stdio" or "hosted"
# /mcp-servers/{serverSlug}/start
{/* Auto-generated */}
Starts a stopped MCP server. Only usable if the server has transport type "stdio" or "hosted"
# /mcp-servers/{serverSlug}/tools
{/* Auto-generated */}
List all cached tools of an MCP server, including the disabled ones
# /mcp-servers/{serverSlug}/source-code
{/* Auto-generated */}
Download the source code of the MCP server as a zip file
# /mcp-servers/{serverSlug}/source-code
{/* Auto-generated */}
Upload source code zip file to update the MCP server source code
# /scim-config/tokens/{tokenId}
{/* Auto-generated */}
Delete a SCIM token and revoke access for every client that uses it. Requires permission to manage the organization
# /secret-stores/{storeId}/test
{/* Auto-generated */}
Test fetching a secret from the store using a secret identifier (e.g., AWS ARN, GCP secret path)
# /secret-stores/{storeId}/mappings
{/* Auto-generated */}
List all secret mappings for a store
# /secret-stores/{storeId}/mappings
{/* Auto-generated */}
Create a new secret mapping
# /service-accounts/{userId}/personal-access-tokens
{/* Auto-generated */}
List the personal access tokens of a service account you may update, from a browser session or a token
# /service-accounts/{userId}/personal-access-tokens
{/* Auto-generated */}
Create a personal access token for a service account you may update and return its API key, from a browser session or a token
# /skill-collections/{collectionId}/shares
{/* Auto-generated */}
List the users and teams a skill collection is shared with and what each grant gives
# /skills/{skillId}/shares
{/* Auto-generated */}
List the users and teams a skill is shared with, and who has access through its collection
# /teams/{teamId}/claim-mappings
{/* Auto-generated */}
List the identity provider claim mappings that give membership of a team at sign-in
# /teams/{teamId}/claim-mappings
{/* Auto-generated */}
Create a claim mapping that gives membership of a team to users with a matching identity provider claim
# /teams/{teamId}/invitations
{/* Auto-generated */}
List the invitations of a team
# /teams/{teamId}/invitations
{/* Auto-generated */}
Invite a person to a team by email. The invitation expires after 7 days and an email is sent to the address
# /teams/{teamId}/members
{/* Auto-generated */}
List the members of a team with their user records. Only organization owners and members of the team can read this
# /teams/{teamId}/members
{/* Auto-generated */}
Add a user to a team with the given role
# /teams/{teamId}/profiles
{/* Auto-generated */}
List the profiles assigned to a team
# /teams/{teamId}/profiles
{/* Auto-generated */}
Assign a profile to a team. Only organization owners can lock the assignment
# /tenants/{tenantId}/subscription
{/* Auto-generated */}
Get tenant subscription details
# /teams/{teamId}/servers
{/* Auto-generated */}
List the MCP server permissions granted to a team
# /users/me/request-email-verification
{/* Auto-generated */}
Send an email verification code using a browser session
# /users/{userId}/personal-access-tokens
{/* Auto-generated */}
List tokens for yourself or a managed service account using a browser session
# /users/{userId}/personal-access-tokens
{/* Auto-generated */}
Create a personal access token and return its API key using a browser session
# /users/{userId}/profiles
{/* Auto-generated */}
List the profiles assigned to a user
# /users/{userId}/profiles
{/* Auto-generated */}
Assign a profile to a user; only organization owners can lock the assignment
# /credentials/{credentialId}/audit-logs
{/* Auto-generated */}
List the audit log entries of a credential, paginated. Requires the read-secret permission on the credential
# /credentials/{id}/refresh
{/* Auto-generated */}
Refresh the OAuth tokens of a credential and store the new token set. Only OAuth credentials can be refreshed, and the read-secret permission is required
# /credentials/{id}/secret
{/* Auto-generated */}
Reveal the secret values of a credential. Requires the read-secret permission on the credential
# /profiles/{profileId}/maintainers/{userId}
{/* Auto-generated */}
Remove a maintainer from the profile. The last remaining maintainer cannot be removed
# /profiles/{profileId}/claim-mappings/{mappingId}
{/* Auto-generated */}
Remove an identity claim mapping from the profile
# /sandboxes/{sandboxId}/audit-logs/{id}
{/* Auto-generated */}
Get a single audit log entry of the sandbox by ID
# /mcp-servers/{serverSlug}/credentials/copy
{/* Auto-generated */}
Copy credentials to another scope
# /mcp-servers/{serverSlug}/credentials/authorize-url
{/* Auto-generated */}
Get an out-of-band OAuth URL which can be opened in any browser on any device to provide authorization credentials for this request's authenticated user. The generated URL will not require user to sign-in to Gatana; instead it will directly start the OAuth authorization flow with the remote MCP server.
# /mcp-servers/{serverSlug}/credentials/{credentialsId}
{/* Auto-generated */}
Delete one specific credential of an MCP server by its ID, whatever its scope is
# /mcp-servers/{serverSlug}/credentials/profile
{/* Auto-generated */}
Get the credentials of an MCP server which are scoped to the profile given in the profileId query parameter. Secret values are not included
# /mcp-servers/{serverSlug}/credentials/token
{/* Auto-generated */}
Get API keys or an OAuth access token; refresh the access token if needed and possible; require server authorization again if the token has expired and has no refresh token; use the current user’s credentials when credentialsId is omitted
# /mcp-servers/{serverSlug}/credentials/user
{/* Auto-generated */}
Delete the credentials of the authenticated user for an MCP server
# /mcp-servers/{serverSlug}/credentials/user
{/* Auto-generated */}
Get the user-scoped credentials of the authenticated user for an MCP server. Secret values are not included
# /mcp-servers/{serverSlug}/credentials/user
{/* Auto-generated */}
Create or replace the credentials of the authenticated user for an MCP server. The credential type must match the authorization method of the server. Servers with transport type "stdio" or "hosted" are restarted to apply the new credentials
# /mcp-servers/{serverSlug}/credentials/server
{/* Auto-generated */}
Delete the server-scoped credentials of an MCP server
# /mcp-servers/{serverSlug}/credentials/server
{/* Auto-generated */}
Get the server-scoped credentials of an MCP server. Secret values are not included
# /mcp-servers/{serverSlug}/credentials/server
{/* Auto-generated */}
Create or replace the server-scoped credentials of an MCP server. The credential type must match the authorization method of the server. Servers with transport type "stdio" or "hosted" are restarted to apply the new credentials
# /mcp-servers/{serverSlug}/files/{fileId}
{/* Auto-generated */}
Delete a file of an MCP server. The file is also removed from the storage bucket
# /mcp-servers/{serverSlug}/files/{fileId}
{/* Auto-generated */}
Download the content of a file
# /mcp-servers/{serverSlug}/files/{fileId}
{/* Auto-generated */}
Replace the contents of an existing file of an MCP server with the uploaded file
# /mcp-servers/{serverSlug}/oauth/discover
{/* Auto-generated */}
Discover OAuth metadata from the given URL or from the remote MCP server URL. With a URL, the server does not need to exist yet
# /mcp-servers/{serverSlug}/openapi/test
{/* Auto-generated */}
Fetch a remote OpenAPI/Swagger specification from the given URL and validate that it is a valid spec
# /mcp-servers/{serverSlug}/tools/{toolName}
{/* Auto-generated */}
Get a single tool of an MCP server
# /mcp-servers/{serverSlug}/tools/{toolName}
{/* Auto-generated */}
Update a single tool of an MCP server
# /scim-config/tokens/{tokenId}/secret
{/* Auto-generated */}
Reveal the raw secret of a SCIM token. Requires permission to manage the organization
# /secret-stores/{storeId}/mappings/{mappingName}
{/* Auto-generated */}
Delete a secret mapping
# /secret-stores/{storeId}/mappings/{mappingName}
{/* Auto-generated */}
Update a secret mapping
# /service-accounts/{userId}/personal-access-tokens/{patId}
{/* Auto-generated */}
Delete a personal access token of a service account you may update, from a browser session or a token
# /teams/{teamId}/claim-mappings/{mappingId}
{/* Auto-generated */}
Delete a team claim mapping
# /teams/{teamId}/invitations/{invitationId}
{/* Auto-generated */}
Delete a team invitation
# /teams/{teamId}/members/{userId}
{/* Auto-generated */}
Remove a member from a team
# /teams/{teamId}/members/{userId}
{/* Auto-generated */}
Change the role of a team member
# /teams/{teamId}/profiles/{profileId}
{/* Auto-generated */}
Remove a profile assignment from a team. Only organization owners can remove a locked assignment
# /users/{userId}/personal-access-tokens/{patId}
{/* Auto-generated */}
Delete a personal access token using a browser session
# /users/{userId}/personal-access-tokens/{patId}
{/* Auto-generated */}
Update a personal access token name or profiles using a browser session
# /users/{userId}/profiles/{profileId}
{/* Auto-generated */}
Remove a profile assignment; only organization owners can remove locked assignments
# /users/{userId}/profiles/{profileId}
{/* Auto-generated */}
Change the lock status of a profile assignment; organization owners only
# /credentials/{credentialId}/audit-logs/{auditLogId}
{/* Auto-generated */}
Get a single audit log entry of a credential. Requires the read-secret permission on the credential
# /servers/{slug}/firewall/test
{/* Auto-generated */}
Test unsaved firewall rules against one call without running the tool or writing audit records
# /artifacts/{artifactId}/shares/{memberType}/{memberId}
{/* Auto-generated */}
Stop sharing an artifact with a user or a team and return who it is still shared with
# /artifacts/{artifactId}/shares/{memberType}/{memberId}
{/* Auto-generated */}
Share an artifact with a user or a team, giving them read access whatever its visibility, and return who it is shared with
# /profiles/{profileId}/servers/{serverSlug}/tools
{/* Auto-generated */}
Replace the tool configuration that the profile uses for one of its MCP servers. The server must be attached to the profile
# /mcp-servers/{serverSlug}/credentials/profiles/{profileId}
{/* Auto-generated */}
Delete the profile-scoped credentials of an MCP server for the given profile
# /mcp-servers/{serverSlug}/credentials/profiles/{profileId}
{/* Auto-generated */}
Create or replace the profile-scoped credentials of an MCP server. The credential type must match the authorization method of the server. Servers with transport type "stdio" or "hosted" are restarted to apply the new credentials
# /mcp-servers/{serverSlug}/credentials/user/apikeys
{/* Auto-generated */}
Get the plaintext API keys of the authenticated user for an MCP server. Only usable if the server uses API key authorization
# /mcp-servers/{serverSlug}/credentials/server/apikeys
{/* Auto-generated */}
Get the plaintext API keys of the server-scoped credentials of an MCP server. Only usable if the server uses API key authorization
# /mcp-servers/{serverSlug}/members/{memberType}/{memberId}
{/* Auto-generated */}
Remove a user or a team from the members of an MCP server
# /mcp-servers/{serverSlug}/members/{memberType}/{memberId}
{/* Auto-generated */}
Add a user or a team as a member of an MCP server, or change the role of an existing member
# /mcp-servers/{serverSlug}/files/{fileId}/name
{/* Auto-generated */}
Rename a file of an MCP server
# /mcp-servers/{serverSlug}/tools/{toolName}/call
{/* Auto-generated */}
Call an enabled MCP tool allowed by the active profiles, validate its arguments, and log the call
# /mcp-servers/{serverSlug}/tools/{toolName}/isEnabled
{/* Auto-generated */}
Enable or disable a single tool of an MCP server. Tells the connected MCP clients that the tool list changed
# /skill-collections/{collectionId}/shares/{memberType}/{memberId}
{/* Auto-generated */}
Stop sharing a skill collection with a user or a team and return who it is still shared with
# /skill-collections/{collectionId}/shares/{memberType}/{memberId}
{/* Auto-generated */}
Share a skill collection with a user or a team as reader or maintainer, which also opens every skill in it, and return who it is shared with
# /skills/{skillId}/shares/{memberType}/{memberId}
{/* Auto-generated */}
Stop sharing a skill with a user or a team and return who it is still shared with
# /skills/{skillId}/shares/{memberType}/{memberId}
{/* Auto-generated */}
Share a skill with a user or a team as reader or maintainer, whatever its visibility, and return who it is shared with
# /mcp-servers/{serverSlug}/credentials/profile/{profileId}/apikeys
{/* Auto-generated */}
Get the plaintext API keys of the profile-scoped credentials of an MCP server. Only usable if the server uses API key authorization