# MCP server for AI assistants

Source: https://ethicsportal.eu/developers/mcp/

The EthicsPortal MCP server lets an AI assistant work with EthicsPortal for a person who approves it. It offers the operations of the [API](https://ethicsportal.eu/developers/api/) as tools of the Model Context Protocol (MCP). The assistant acts as that person, with their current role, only in the organisations they choose. Report content is never available.

## Status

The MCP server is available to every organisation that has API access. It opened on 11 November 2026.

* EthicsPortal has tested it with Claude Code. Other assistants that support remote MCP servers with OAuth are expected to work. Write to [support@ethicsportal.eu](mailto:support@ethicsportal.eu) if one does not.
* If API access is suspended for an organisation, the person cannot choose it on the consent screen, and its tools answer `not_found`.
* If EthicsPortal switches assistant connections off, the MCP endpoint and every OAuth address on this page answer `404`.

The REST API and webhooks do not depend on the MCP server. [Choose an API token](https://ethicsportal.eu/developers/authentication/) for scripts and server integrations.

## Add EthicsPortal to an assistant

A person adds the server to their assistant once. They need no EthicsPortal account first: a person with no account [signs up while they connect](https://ethicsportal.eu/developers/mcp/#start-without-an-organisation).

| Assistant           | How to add the server                                                                                                                         |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude Code         | Run claude mcp add --transport http ethicsportal https://secure.ethicsportal.eu/mcp. Then open /mcp, choose ethicsportal and **Authenticate** |
| Cursor              | Add the server to \~/.cursor/mcp.json, as shown below. Sign in when Cursor asks                                                               |
| Claude              | **Settings → Connectors → Add custom connector**. Enter the address https://secure.ethicsportal.eu/mcp                                        |
| Any other assistant | Add https://secure.ethicsportal.eu/mcp as a remote MCP server or custom connector in the assistant’s settings                                 |

```
{ "mcpServers": { "ethicsportal": { "url": "https://secure.ethicsportal.eu/mcp" } } }
```

The assistant then opens a browser window. The person signs in or signs up there, reads the consent screen and presses **Connect**.

An assistant cannot use a server that nobody added. A coding assistant can add the server itself, because it can run the command or change its configuration. A chat assistant cannot: the person adds the connector in its settings. The [setup skill](https://ethicsportal.eu/.well-known/agent-skills/ethicsportal-setup/SKILL.md) gives an assistant these steps, and the steps that follow, in a form it can read.

## Endpoint and transport

| Property      | Value                                                                     |
| ------------- | ------------------------------------------------------------------------- |
| Endpoint      | https://secure.ethicsportal.eu/mcp                                        |
| Transport     | Streamable HTTP, POST only                                                |
| Request       | One JSON-RPC message per request. A batch is refused                      |
| Response      | One JSON document, or no body. The server opens no stream                 |
| Session       | None. The server issues no Mcp-Session-Id                                 |
| Authorization | Authorization: Bearer with an OAuth access token issued for this endpoint |
| Features      | Tools only. No prompts and no resources                                   |

Send `Content-Type: application/json` and `Accept: application/json, text/event-stream` with every request.

Two lifecycles work:

* **Revision `2026-07-28`**, without a handshake. Each request carries its protocol revision, and `server/discover` describes the server.
* **Revisions `2025-11-25` and `2025-06-18`**, with the `initialize` handshake. Send the `MCP-Protocol-Version` header on the requests that follow. A client that asks for `2026-07-28` in `initialize` is offered `2025-11-25`.

The server returns short instructions for the assistant in both lifecycles. They tell it to start with `get_identity`.

```
curl --fail-with-body https://secure.ethicsportal.eu/mcp \
  -H "Authorization: Bearer $ETHICSPORTAL_MCP_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/call' \
  -H 'Mcp-Name: get_identity' \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_identity","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
```

Most clients do not build these requests themselves. Add the endpoint address as a remote MCP server in the assistant. The assistant must support OAuth 2.1 with PKCE and Dynamic Client Registration.

## Connect a client

A connection is made with OAuth 2.1\. The client is a public client: it has no secret. The steps are discovery, registration, the authorization request, and the token exchange.

### Discovery

A request without a usable token answers `401`. The `WWW-Authenticate` header names the protected resource metadata (RFC 9728):

```
WWW-Authenticate: Bearer scope="reports:read channel:read channel:write webhooks:read webhooks:write audit_log:read members:read members:write documents:read billing:read billing:write organizations:write organizations:create", resource_metadata="https://secure.ethicsportal.eu/.well-known/oauth-protected-resource/mcp"
```

```
curl --fail-with-body https://secure.ethicsportal.eu/.well-known/oauth-protected-resource/mcp
```

```
{
  "resource": "https://secure.ethicsportal.eu/mcp",
  "authorization_servers": ["https://secure.ethicsportal.eu"],
  "scopes_supported": [
    "reports:read",
    "channel:read",
    "channel:write",
    "webhooks:read",
    "webhooks:write",
    "audit_log:read",
    "members:read",
    "members:write",
    "documents:read",
    "billing:read",
    "billing:write",
    "organizations:write",
    "organizations:create"
  ],
  "bearer_methods_supported": ["header"]
}
```

`scopes_supported` lists every scope a connection can hold, here and in the authorization server metadata. The same document is also at `/.well-known/oauth-protected-resource`. The authorization server metadata (RFC 8414) is at the issuer:

```
curl --fail-with-body https://secure.ethicsportal.eu/.well-known/oauth-authorization-server
```

The response includes these fields:

```
{
  "issuer": "https://secure.ethicsportal.eu",
  "authorization_endpoint": "https://secure.ethicsportal.eu/oauth/authorize",
  "token_endpoint": "https://secure.ethicsportal.eu/oauth/token",
  "revocation_endpoint": "https://secure.ethicsportal.eu/oauth/revoke",
  "registration_endpoint": "https://secure.ethicsportal.eu/oauth/register",
  "scopes_supported": [
    "reports:read",
    "channel:read",
    "channel:write",
    "webhooks:read",
    "webhooks:write",
    "audit_log:read",
    "members:read",
    "members:write",
    "documents:read",
    "billing:read",
    "billing:write",
    "organizations:write",
    "organizations:create"
  ],
  "response_types_supported": ["code"],
  "response_modes_supported": ["query"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none"],
  "code_challenge_methods_supported": ["S256"],
  "authorization_response_iss_parameter_supported": true,
  "resource_indicators_supported": true
}
```

There is no token introspection endpoint.

### Register the client

Clients register themselves with Dynamic Client Registration (RFC 7591). Client ID Metadata Documents are not supported yet.

```
curl --fail-with-body https://secure.ethicsportal.eu/oauth/register \
  -H 'Content-Type: application/json' \
  --data '{"client_name":"Example assistant","redirect_uris":["https://assistant.example/callback"],"token_endpoint_auth_method":"none","grant_types":["authorization_code","refresh_token"],"response_types":["code"]}'
```

A `201` response contains the `client_id`. There is no client secret.

```
{
  "client_id": "CLIENT_ID",
  "client_id_issued_at": 1794268800,
  "client_name": "Example assistant",
  "redirect_uris": ["https://assistant.example/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

| Field                         | Accepted                                                                                                                                |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| redirect\_uris                | Required. One to ten addresses, 4,000 characters in total                                                                               |
| client\_name                  | Optional. Up to 100 characters, without control or invisible characters. Without a name, the host of the first redirect address is used |
| token\_endpoint\_auth\_method | Omitted, or none                                                                                                                        |
| grant\_types                  | Omitted, or only authorization\_code and refresh\_token                                                                                 |
| response\_types               | Omitted, or only code                                                                                                                   |

Every other field is ignored and is not stored.

A redirect address must use `https`, or `http` on a loopback host: `localhost`, `127.0.0.1` or `::1`. It must not contain user information or a fragment, and it must not point to an EthicsPortal host. Custom URL schemes are refused. The only exception is the exact address `cursor://anysphere.cursor-mcp/oauth/callback`; no other address with that scheme is accepted. A refused address answers `400` with `invalid_redirect_uri`. Any other refused field answers `400` with `invalid_client_metadata`.

A registration that has no connection is deleted when it is 30 days old. If a client ID is no longer recognised, register again.

### Ask for authorization

Send the person’s browser to the authorization endpoint:

```
https://secure.ethicsportal.eu/oauth/authorize
  ?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=https%3A%2F%2Fassistant.example%2Fcallback
  &code_challenge=CODE_CHALLENGE
  &code_challenge_method=S256
  &resource=https%3A%2F%2Fsecure.ethicsportal.eu%2Fmcp
  &scope=reports%3Aread%20channel%3Aread
  &state=STATE
```

| Parameter                                | Rule                                                                                                |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------- |
| response\_type                           | code only                                                                                           |
| redirect\_uri                            | A registered address. For a loopback address, the port can differ from the registered one           |
| code\_challenge, code\_challenge\_method | PKCE is required, with S256 only. plain is refused                                                  |
| resource                                 | Required. Exactly https://secure.ethicsportal.eu/mcp                                                |
| scope                                    | Optional. Without it, the request asks for every scope a connection can hold. See [Scopes](https://ethicsportal.eu/developers/mcp/#scopes) |
| response\_mode                           | Omitted, or query                                                                                   |

A request that breaks one of these rules is refused on an error page in EthicsPortal. The browser is not sent back to the client, so the client receives no error redirect.

### What the person sees

1. The person signs in to EthicsPortal if they are not signed in, and completes a fresh-authentication challenge. A person with no account creates one at this step: they confirm their email address, accept the account terms and enter their name. Then they return to the same request.
2. The consent screen shows the name of the app and the host of its redirect address. It states that the app chose both and that EthicsPortal has not verified them.
3. It lists the permissions the app asks for, in plain language, and the limits of a connection.
4. It lists the organisations the person can connect. These are the organisations where the person is an active member and API access is on. The person ticks at least one.
5. **Connect** sends the browser back to the redirect address with `code`, `state` and `iss`. **Cancel** sends it back with `error=access_denied`.

Consent is asked every time. An earlier connection never approves a new request by itself.

After each approval, the person receives one email. It names the app and its address, the time, the organisations, the permissions and the end date, and it links to **Connected apps**, where the person can end the connection.

### Start without an organisation

A person who has no organisation in EthicsPortal can connect an assistant, and the assistant creates their first organisation.

1. The authorization request must include `organizations:create`. A request that follows the metadata includes it.
2. The consent screen shows no list of organisations. It says that the app can create one, and that the person accepts its Data Processing Agreement afterwards.
3. The connection reaches no organisation at first. `get_identity` returns an empty `organizations` list.
4. `create_organization` creates the organisation with the person as its owner, and adds it to the connection.
5. The owner accepts the organisation’s Data Processing Agreement in a browser, at `https://secure.ethicsportal.eu/organizations/{organization_id}`. Until then, the organisation’s tools answer `agreement_required`. The assistant cannot accept it.
6. After that, the assistant can configure the whistleblowing channel, invite members and create a checkout link, with the scopes the person approved.

The tool results guide the assistant through steps 3 to 5\. The empty `get_identity` result, the `create_organization` result and every `agreement_required` answer carry a `next_step` text: what to do next, and the address the person must open. The text is in the result’s content and in `structuredContent.next_step`. The REST API does not return this field.

### Exchange the code

The authorization code is valid for one minute and for one use.

```
curl --fail-with-body https://secure.ethicsportal.eu/oauth/token \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "code=$CODE" \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode 'redirect_uri=https://assistant.example/callback' \
  --data-urlencode "code_verifier=$CODE_VERIFIER" \
  --data-urlencode 'resource=https://secure.ethicsportal.eu/mcp'
```

```
{
  "access_token": "ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "REFRESH_TOKEN",
  "scope": "reports:read channel:read",
  "created_at": 1794268800
}
```

The access token works at the MCP endpoint only. The REST API refuses it with `401`. A `resource` other than the MCP endpoint answers `invalid_target`.

### Refresh and revoke tokens

```
curl --fail-with-body https://secure.ethicsportal.eu/oauth/token \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode "refresh_token=$REFRESH_TOKEN" \
  --data-urlencode "client_id=$CLIENT_ID"
```

Each refresh returns a new access token and a new refresh token. The earlier access token and refresh token stop working at once. Store the new pair before you use it.

* A refresh cannot change the scopes. If you send `scope`, it must name exactly the scopes of the connection, or the answer is `invalid_scope`.
* A refresh token works once. There is no period in which the earlier one is still accepted.
* Other grant types answer `unsupported_grant_type`.

To end a connection from the client, revoke its current access token or refresh token (RFC 7009):

```
curl --fail-with-body https://secure.ethicsportal.eu/oauth/revoke \
  --data-urlencode "token=$REFRESH_TOKEN" \
  --data-urlencode "client_id=$CLIENT_ID"
```

A client can revoke only its own tokens. Another client’s token answers `403`.

### Lifetimes

| Item               | Lifetime                                                |
| ------------------ | ------------------------------------------------------- |
| Authorization code | One minute, one use                                     |
| Access token       | One hour                                                |
| Refresh token      | One use. Each refresh returns a new one                 |
| Connection         | 90 days from the approval. A refresh does not extend it |

### Browser-based clients

A client that runs in a web page on another origin can connect. The MCP endpoint, both metadata documents and the registration, token and revocation endpoints answer with `Access-Control-Allow-Origin: *` and answer an `OPTIONS` preflight with `204`. They never use cookies, and they send no `Access-Control-Allow-Credentials` header. The MCP endpoint and the protected resource metadata expose the `WWW-Authenticate` and `Retry-After` headers to the page.

The consent screen has no such headers. Open it as a page in the person’s browser.

## What ends a connection

When a connection ends, its access tokens answer `401` and its refresh tokens answer `invalid_grant`. The person must approve a new authorization request.

* 90 days have passed since the approval.
* The person disconnects the app under **Connected apps**.
* An administrator removes the connection from the last organisation it reaches.
* The person’s membership is deactivated in the last organisation it reaches.
* The client revokes its current access token or refresh token.
* **A refresh token is used a second time.** This includes a client that repeats a refresh request after a lost response.
* An authorization code is exchanged a second time with its PKCE verifier.
* The person approves a new request from the same app. The new connection replaces the earlier one, and the 90 days start again.

A connection stops working, without ending, while the person’s account cannot authenticate or while assistant connections are switched off.

## What a connection can do

A connection acts as the person who approved it. Every tool call checks that person’s current role and case permissions again, as described in [Case permissions](https://ethicsportal.eu/developers/authentication/#case-permissions). A connection never does more than the person can do in the web application.

* It reaches only the organisations the person ticked. Any other organisation answers `not_found`, also when the person is a member of it.
* An organisation that the person joins later is not added. The one exception is an organisation created with the `create_organization` tool of that connection.
* When the person’s membership in an organisation is deactivated, that organisation is removed from the connection.
* The person’s account terms and the organisation’s agreement must be accepted, as for the API. Otherwise the tool answers `agreement_required`.

Tool descriptions come from the API reference. Where a description says “personal token”, it applies to a connection in the same way.

### Scopes

A connection uses the scopes of the API. The [scope table](https://ethicsportal.eu/developers/authentication/#scopes) describes each one. A connection can hold these:

`reports:read`, `channel:read`, `channel:write`, `webhooks:read`, `webhooks:write`, `audit_log:read`, `members:read`, `members:write`, `documents:read`, `billing:read`, `billing:write`, `organizations:write`, `organizations:create`

* `invitations:respond` cannot be granted. Accepting an invitation joins an organisation that the person did not choose on the consent screen. A request that asks for it is refused.
* `get_identity` and `submit_feedback` need no scope.
* `members:write` lets the app send invitations with any role, in the organisations where the person is an administrator. A person who is invited as an administrator can read reports in the web application after they accept. The consent screen warns the person when an app asks for this scope, and each invitation is recorded in the audit trail as made through an AI assistant.
* The metadata advertises every scope, so a client that follows it asks for all of them in the first authorization request. Most assistants make the person sign in again for each scope added later. A client can ask for fewer scopes. The consent screen always shows every scope the client asks for.

### Ask for a further scope

A connection that asked for fewer scopes can ask for more later. When a tool needs a scope the connection does not have, nothing runs. The whole HTTP response is `403`:

```
WWW-Authenticate: Bearer error="insufficient_scope", error_description="This tool needs the webhooks:write scope.", scope="reports:read channel:read webhooks:write", resource_metadata="https://secure.ethicsportal.eu/.well-known/oauth-protected-resource/mcp"
```

1. Read `scope`. It names the scopes the connection already has and the missing one.
2. Send a new authorization request with exactly that `scope`.
3. The person approves on the consent screen. The organisations of the earlier connection are ticked already.
4. Exchange the new code, then call the tool again with the new access token.

The new connection replaces the earlier one. The earlier tokens stop working, the 90 days start again, and the person receives a new email.

## Tools

The server offers one tool for each API operation, with the exceptions listed below. Tool definitions are generated from the [OpenAPI schema](https://ethicsportal.eu/openapi.json), so a tool has the same fields, rules and errors as its operation. Follow the link of a tool to its page in the API reference.

* The tool name is the `operationId` of the operation in snake case: `listReports` becomes `list_reports`.
* The tool title and description are the summary and description of the operation.
* Tools that read are marked read-only. Tools that send `PATCH` or `DELETE` are marked destructive and idempotent.
* `tools/list` returns the tools in the order of the OpenAPI schema.

| Tool                           | API operation                                                                                | Scope                |
| ------------------------------ | -------------------------------------------------------------------------------------------- | -------------------- |
| get\_identity                  | [Discover organisations](https://ethicsportal.eu/developers/api/operations/getidentity/)                            | None                 |
| get\_channel                   | [Read whistleblowing channel metadata](https://ethicsportal.eu/developers/api/operations/getchannel/)               | channel:read         |
| list\_reports                  | [List report metadata](https://ethicsportal.eu/developers/api/operations/listreports/)                              | reports:read         |
| get\_report                    | [Read report metadata](https://ethicsportal.eu/developers/api/operations/getreport/)                                | reports:read         |
| create\_organization           | [Create an organisation](https://ethicsportal.eu/developers/api/operations/createorganization/)                     | organizations:create |
| get\_channel\_configuration    | [Read channel configuration](https://ethicsportal.eu/developers/api/operations/getchannelconfiguration/)            | channel:write        |
| update\_channel\_configuration | [Configure a whistleblowing channel](https://ethicsportal.eu/developers/api/operations/updatechannelconfiguration/) | channel:write        |
| list\_webhooks                 | [List webhooks](https://ethicsportal.eu/developers/api/operations/listwebhooks/)                                    | webhooks:read        |
| create\_webhook                | [Create a webhook](https://ethicsportal.eu/developers/api/operations/createwebhook/)                                | webhooks:write       |
| get\_webhook                   | [Read a webhook](https://ethicsportal.eu/developers/api/operations/getwebhook/)                                     | webhooks:read        |
| update\_webhook                | [Update a webhook](https://ethicsportal.eu/developers/api/operations/updatewebhook/)                                | webhooks:write       |
| delete\_webhook                | [Delete a webhook](https://ethicsportal.eu/developers/api/operations/deletewebhook/)                                | webhooks:write       |
| rotate\_webhook\_secret        | [Rotate a webhook's signing secret](https://ethicsportal.eu/developers/api/operations/rotatewebhooksecret/)         | webhooks:write       |
| test\_webhook                  | [Send a test event](https://ethicsportal.eu/developers/api/operations/testwebhook/)                                 | webhooks:write       |
| list\_webhook\_deliveries      | [List webhook deliveries](https://ethicsportal.eu/developers/api/operations/listwebhookdeliveries/)                 | webhooks:read        |
| retry\_webhook\_delivery       | [Retry a failed delivery](https://ethicsportal.eu/developers/api/operations/retrywebhookdelivery/)                  | webhooks:write       |
| list\_audit\_trail             | [List the audit trail](https://ethicsportal.eu/developers/api/operations/listaudittrail/)                           | audit\_log:read      |
| list\_report\_audit\_trail     | [List a report's audit trail](https://ethicsportal.eu/developers/api/operations/listreportaudittrail/)              | audit\_log:read      |
| list\_members                  | [List members](https://ethicsportal.eu/developers/api/operations/listmembers/)                                      | members:read         |
| list\_invitations              | [List pending invitations](https://ethicsportal.eu/developers/api/operations/listinvitations/)                      | members:read         |
| create\_invitation             | [Invite a member](https://ethicsportal.eu/developers/api/operations/createinvitation/)                              | members:write        |
| revoke\_invitation             | [Revoke an invitation](https://ethicsportal.eu/developers/api/operations/revokeinvitation/)                         | members:write        |
| resend\_invitation             | [Resend an invitation](https://ethicsportal.eu/developers/api/operations/resendinvitation/)                         | members:write        |
| update\_organization           | [Rename the organisation](https://ethicsportal.eu/developers/api/operations/updateorganization/)                    | organizations:write  |
| list\_documents                | [List documents](https://ethicsportal.eu/developers/api/operations/listdocuments/)                                  | documents:read       |
| get\_billing\_status           | [Read billing status](https://ethicsportal.eu/developers/api/operations/getbillingstatus/)                          | billing:read         |
| create\_checkout\_link         | [Create a subscription checkout link](https://ethicsportal.eu/developers/api/operations/createcheckoutlink/)        | billing:write        |
| create\_billing\_portal\_link  | [Create a billing portal link](https://ethicsportal.eu/developers/api/operations/createbillingportallink/)          | billing:write        |
| submit\_feedback               | [Send product feedback](https://ethicsportal.eu/developers/api/operations/submitfeedback/)                          | None                 |

### Arguments

* **Path parameters**, such as `organization_id`, are record IDs. Take them from `get_identity` and from earlier results. A value that is not a record ID is refused before anything runs.
* **Query parameters**, such as `limit`, `cursor` and `status`, are arguments with the same names.
* **The request body** keeps its shape. `create_webhook` takes a `webhook` object, as the API does.
* **`idempotency_key`** is an argument of every tool whose operation accepts the `Idempotency-Key` header.
* Arguments that the tool does not declare are refused.

An `Idempotency-Key` header that the client sends to the MCP endpoint does not reach the operation: use the argument. Conditional requests with `If-None-Match` are not available through tools.

The `params` of a `tools/call` request:

```
{
  "name": "list_reports",
  "arguments": { "organization_id": "gHiJkl", "status": "received", "limit": 50 }
}
```

### Results and errors

A successful call returns the JSON response of the API in `structuredContent`. The same JSON is in `content` as text. An operation that returns no content answers with the text `Done.`

When the API refuses the call, the result has `isError: true`, and `structuredContent` holds the [error body of the API](https://ethicsportal.eu/developers/pagination-and-errors/#errors). The HTTP status of the MCP response is still `200`, so read `error.code`:

```
{
  "content": [
    {
      "type": "text",
      "text": "{\"error\":{\"code\":\"validation_failed\",\"fields\":[\"url\"],\"feedback_url\":\"https://secure.ethicsportal.eu/me/feedback.json\"}}"
    }
  ],
  "structuredContent": {
    "error": {
      "code": "validation_failed",
      "fields": ["url"],
      "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json"
    }
  },
  "isError": true
}
```

`feedback_url` is the address of the product feedback operation. Through MCP, use the `submit_feedback` tool for the same purpose.

Through a connection, `get_identity` returns `type` `connection`, the name of the app as `name`, and `last_four` `null`. Its `expires_at` is the end of the connection, and `organizations` lists only the organisations the connection reaches.

### Pagination, retries and rate limits

* **Pagination.** A list returns `next_cursor`. Call the tool again with `cursor` set to that value until it is `null`. [Pagination](https://ethicsportal.eu/developers/pagination-and-errors/#pagination).
* **Safe retries.** Send `idempotency_key` with every tool that accepts it, and send the same key again when you retry. The rules are those of the API: [Safe retries](https://ethicsportal.eu/developers/pagination-and-errors/#safe-retries). Keys belong to the connection. A new approval creates a new connection, so earlier keys do not carry over.
* **Rate limits of the endpoint.** Each connection can send 120 MCP requests per minute. Requests without a usable token are limited to 120 per minute per source address. Above the limit the HTTP response is `429` with `Retry-After: 60`.
* **Rate limits of the API.** Tool calls also count toward the [API limits](https://ethicsportal.eu/developers/pagination-and-errors/#rate-limits): 60 per minute per connection and 300 per minute per organisation. Above these, the tool result is an error with the code `rate_limited`.

### Operations without a tool

| API operation                                                                                    | Why it has no tool                                                                                   |
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| [List my invitations](https://ethicsportal.eu/developers/api/operations/listmyinvitations/)                             | It needs invitations:respond, which no connection can hold.                                          |
| [Accept an invitation](https://ethicsportal.eu/developers/api/operations/acceptinvitation/)                             | It needs invitations:respond, which no connection can hold.                                          |
| [Decline an invitation](https://ethicsportal.eu/developers/api/operations/declineinvitation/)                           | It needs invitations:respond, which no connection can hold.                                          |
| [Download the compliance report](https://ethicsportal.eu/developers/api/operations/downloadcompliancereport/)           | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. |
| [Download the compliance certificate](https://ethicsportal.eu/developers/api/operations/downloadcompliancecertificate/) | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. |
| [Download the whistleblowing policy](https://ethicsportal.eu/developers/api/operations/downloadwhistleblowerpolicy/)    | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. |
| [Download the privacy notice](https://ethicsportal.eu/developers/api/operations/downloadprivacynotice/)                 | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. |
| [Download the poster](https://ethicsportal.eu/developers/api/operations/downloadposter/)                                | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. |
| [Download the case handler manual](https://ethicsportal.eu/developers/api/operations/downloadcasehandlermanual/)        | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. |

`list_documents` returns the address of each document. An access token of a connection cannot download it, because the API refuses that token. A signed-in person can open the address in a browser, and a script can download it with an [API token](https://ethicsportal.eu/developers/authentication/).

Billing tools return a link to a Stripe-hosted page for a person to open. They never take a payment.

## Data boundary

The MCP server has the same [data boundary](https://ethicsportal.eu/developers/data-boundaries/) as the API. Report content, reporter identity, messages, notes and attachments are never available. No tool reads report content, and no tool submits or changes a report.

A tool returns the response of its API operation, so every field that the API excludes is also absent here. Excluded fields are omitted entirely. They are never returned as `REDACTED`, and there is no expansion option.

No tool returns report content. One permission can give another person access to reports in the web application: see `members:write` under [Scopes](https://ethicsportal.eu/developers/mcp/#scopes).

Operational metadata can still identify a case inside your organisation. A tool result leaves EthicsPortal and goes to the assistant that the person connected. Administrators can see and remove these connections under **Connected apps**.

## Manage connections

* **The person** sees their connections in account settings under **Connected apps**. Each entry shows the app, its address, the organisations, the permissions, the dates and the last activity. **Disconnect** ends the connection in every organisation.
* **An administrator** sees every connection that reaches the organisation under Organisation → Integrations → **Connected apps**, with the member who connected it. **Remove access** removes that organisation from the connection. The person’s other organisations are not affected.
* **The audit trail.** Where the API records a request in the audit trail, it records the same tool call. The entry names the person and is marked **AI assistant**, with the name and address of the app. In the [audit trail API](https://ethicsportal.eu/developers/administration/#read-the-audit-trail), the actor’s kind is `mcp`. Approving, removing and ending a connection is recorded in the audit trail of each organisation it reaches.

## Errors and troubleshooting

HTTP responses of the MCP endpoint:

| Status   | Meaning                                                                                                                                          | Action                                                                                             |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| 400      | The body is not one JSON-RPC request, or the protocol revision is not supported                                                                  | Send one request per POST. The error lists the supported revisions                                 |
| 401      | There is no token, or the token cannot be used: it is unknown, expired or issued for another resource, or its connection has ended               | Refresh the access token. If the refresh answers invalid\_grant, start a new authorization request |
| 403      | The tool needs a scope the connection does not have                                                                                              | [Ask for a further scope](https://ethicsportal.eu/developers/mcp/#ask-for-a-further-scope)                                                |
| 404      | Assistant connections are switched off. In the 2026-07-28 lifecycle, also a JSON-RPC method that the server does not offer, such as prompts/list | Try again later, or write to support. Use tools only                                               |
| 405      | The method is GET or DELETE. The response has Allow: POST                                                                                        | Send POST                                                                                          |
| 406, 415 | The Accept or Content-Type header is not accepted                                                                                                | Send the headers shown under [Endpoint and transport](https://ethicsportal.eu/developers/mcp/#endpoint-and-transport)                     |
| 413      | The request body is larger than 1 MB                                                                                                             | Send a smaller request                                                                             |
| 429      | The connection or the source address sent too many requests                                                                                      | Wait for the Retry-After interval                                                                  |

A tool error arrives with HTTP `200` and `isError: true`. The codes are those of the [API errors](https://ethicsportal.eu/developers/pagination-and-errors/#errors). The most frequent:

| Code                | Meaning                                                                                                                            |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| not\_found          | The record does not exist, the person cannot access it, the organisation is not part of the connection, or API access is suspended |
| forbidden           | The person’s current role does not allow the action                                                                                |
| agreement\_required | The person or the organisation must accept an agreement in the web application                                                     |
| validation\_failed  | Correct the fields named in fields                                                                                                 |
| rate\_limited       | Wait one minute                                                                                                                    |

OAuth endpoints:

| Where                           | Answer                                         | Meaning                                                                                                                                                                                                                               |
| ------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Registration                    | 400 invalid\_redirect\_uri                     | A redirect address is missing or not accepted                                                                                                                                                                                         |
| Registration                    | 400 invalid\_client\_metadata                  | The client is not a public client, or a field is not accepted                                                                                                                                                                         |
| Registration, token, revocation | 413                                            | The request body is larger than 16 KB                                                                                                                                                                                                 |
| Authorization                   | Error page                                     | The request is not valid. The browser is not sent back to the client                                                                                                                                                                  |
| Authorization                   | Page “Assistant connections are not available” | The person has no organisation with API access, and the request does not include organizations:create                                                                                                                                 |
| Token                           | invalid\_grant                                 | The code or refresh token is unknown, expired or already used, the PKCE verifier is wrong, or the connection has ended                                                                                                                |
| Token                           | invalid\_target                                | resource is not the MCP endpoint                                                                                                                                                                                                      |
| Token                           | invalid\_scope                                 | A refresh asked for scopes other than those of the connection                                                                                                                                                                         |
| Any                             | 429                                            | Too many requests. Registration allows 20 per hour and authorization 30 per minute per source address. The token and revocation endpoints allow 60 per minute per client at one source address, and 600 per minute per source address |
