Skip to main content
Glama
README.md
# netcup-mcp

An [MCP](https://modelcontextprotocol.io) server for the [netcup CCP domain
webservice](https://ccp.netcup.net/run/webservice/servers/endpoint.php).
It exposes the netcup domain API as tools, so an agent can list domains,
read and edit DNS zones, and manage contact handles. Multiple netcup
accounts are supported through a config file.

Built with Python and [uv](https://docs.astral.sh/uv/). Two runtime
dependencies: [`mcp`](https://pypi.org/project/mcp/) and `httpx`.

## Quick start

```sh
git clone https://github.com/twink0r/netcup-mcp
cd netcup-mcp
uv sync
```

Grab your customer number, API key and API password from the netcup CCP
under **API / Webservice**, then put them in a config file:

```sh
mkdir -p ~/.config/netcup-mcp
cp netcup-mcp.example.toml netcup-mcp.toml

cat > ~/.config/netcup-mcp/default.env <<'EOF'
NETCUP_CUSTOMERNUMBER=123456
NETCUP_APIKEY=your-api-key
NETCUP_APIPASSWORD=your-api-password
EOF

chmod 600 ~/.config/netcup-mcp/default.env
```

Edit `netcup-mcp.toml` so it points at that file:

```toml
default_account = "default"

[accounts.default]
description = "My netcup account"
env_file = "~/.config/netcup-mcp/default.env"
```

Check it resolves, without starting a server:

```sh
uv run netcup-mcp --list-accounts
```

Then point your MCP client at it (see [Prime Agent](#prime-agent) or the
[other clients](#other-mcp-clients) section).

### Without a config file

For a single account you can skip the config file entirely and use
environment variables:

```sh
export NETCUP_CUSTOMERNUMBER=123456
export NETCUP_APIKEY=your-api-key
export NETCUP_APIPASSWORD=your-api-password
uv run netcup-mcp
```

Note that some MCP hosts do not pass arbitrary environment variables to a
server process. If yours does not, use a config file.

## Notes on the netcup API

The webservice is SOAP, but the same endpoint answers JSON when called with
`?JSON`, so this server needs no XML stack.

### Three undocumented requirements that will bite you

All three were found by testing against the live API, and all three fail in
confusing ways:

1. **Parameters go under a `param` key.** The payload must be
   `{"action": "infoDomain", "param": {...}}`. A flat object returns
   `4013 Invalid entry for field apikey` **even when the credentials are
   correct** — so a bad password and a wrong request shape look identical.
2. **`customernumber` must be a JSON number**, not a string, otherwise you get
   `4005 Customer number in invalid format`.
3. **`clientrequestid` must always be present**, despite being documented as
   optional. Without it netcup returns HTTP 500.

The client handles all three; a tool call never has to care.

## Tools

Read-only: `list_accounts`, `listall_domains`, `listall_handle`,
`info_domain`, `info_handle`, `info_dns_zone`, `info_dns_records`,
`price_topleveldomain`, `poll`, `get_authcode_domain`.

Write: `update_dns_records`, `update_dns_zone`, `update_domain`,
`create_handle`, `update_handle`, `delete_handle`, `change_owner_domain`,
`cancel_domain`, `transfer_domain`, `create_domain`.

`login` and `logout` are handled by the server itself: it opens a session on
first use, caches it per account, and re-authenticates once if the session
expires. Credentials never reach the model.

## Configuration

Copy `netcup-mcp.example.toml` to `netcup-mcp.toml`:

```toml
default_account = "prod"

[accounts.prod]
description = "Production netcup account"
env_file = "~/.config/netcup-mcp/prod.env"

[accounts.staging]
description = "Staging netcup account"
customernumber = "123456"
apikey = "${NETCUP_STAGING_APIKEY}"
apipassword = "${NETCUP_STAGING_APIPASSWORD}"
```

An account can hold its credentials **inline** or point at an **env file**,
or mix both. Inline values win over `env_file`.

Both the config and the env files expand `${VAR}` against the process
environment, so a secret does not have to be written down twice.

Env files are ordinary `KEY=value` files. `export`, `#` comments, and quoted
values all work:

```sh
# ~/.config/netcup-mcp/prod.env
NETCUP_CUSTOMERNUMBER=123456
NETCUP_APIKEY=your-key
NETCUP_APIPASSWORD="your-password"
```

Relative `env_file` paths resolve against the config file's directory.

The config file is found in this order:

1. `--config PATH`
2. `$NETCUP_MCP_CONFIG`
3. `./netcup-mcp.toml` in the working directory
4. `$XDG_CONFIG_HOME/netcup-mcp/config.toml`, else `~/.config/netcup-mcp/config.toml`

If no config file is found, the server falls back to the three
`NETCUP_*` environment variables as a single account named `default`.

## Multiple accounts

Every tool takes an optional `account` argument. Leave it out to use
`default_account`. Each account gets its own API session, so switching
accounts never reuses the wrong session.

The model should call `list_accounts` to discover the configured names
rather than guessing one.

```sh
netcup-mcp --list-accounts                  # show what is configured
netcup-mcp --account staging                # override the default for this run
```

## Command line

```text
netcup-mcp [--config PATH] [--account NAME] [--list-accounts]
```

## Run

```sh
uv run netcup-mcp
```

The server speaks MCP over stdio.

## Other MCP clients

The server speaks stdio, so any MCP client works. Examples:

**Claude Code**

```sh
claude mcp add netcup -- uv --directory /path/to/netcup-mcp run netcup-mcp \
  --config /path/to/netcup-mcp/netcup-mcp.toml
```

**Generic JSON client config**

```json
{
  "mcpServers": {
    "netcup": {
      "command": "uv",
      "args": ["--directory", "/path/to/netcup-mcp", "run", "netcup-mcp",
               "--config", "/path/to/netcup-mcp/netcup-mcp.toml"]
    }
  }
}
```

## Prime Agent

The server is registered as a stdio MCP server in Prime Agent:

```sh
prime-agent mcp add netcup --cwd /path/to/netcup-mcp -- \
  uv run netcup-mcp --config /path/to/netcup-mcp/netcup-mcp.toml
```

That writes an `mcpServers.netcup` entry to `~/.prime/agent/settings.json`.
Remove it again with `prime-agent mcp remove netcup`.

Call the tools from the agent's Python kernel through the pre-imported
`mcp` module. Note that inside Prime Agent the name `mcp` is already the
MCP **SDK**; the Prime Agent service module is `rlm.mcp`:

```python
from rlm import mcp

tools = await mcp.list_tools("netcup")
result = await mcp.call_tool("netcup", "list_accounts", {})
result = await mcp.call_tool("netcup", "info_domain",
                             {"domainname": "example.com", "account": "prod"})
```

`mcp.reload()` re-reads the settings and closes open connections, which
picks up config changes without restarting the session.

### Account permissions

The netcup API gates many functions behind a reseller account. On a normal
customer account, calls like `listall_domains` return
`4020 Function not available - This function is available for resellers`.
That is an API answer, not a bug in this server.

DNS tools have a second restriction: a zone can only be read or written when
the domain uses netcup's own nameservers. A domain delegated elsewhere (for
example to Cloudflare) returns
`5029/5031 ... Domain uses external name servers`.

### One gotcha: environment variables do not cross the boundary

Prime Agent passes a stdio child only `HOME`, `PATH`, `TMPDIR`, `TEMP`, `TMP`
and any explicit `env` references. **`NETCUP_CUSTOMERNUMBER`,
`NETCUP_APIKEY` and `NETCUP_APIPASSWORD` are not passed through**, so the
environment fallback does not work when the server runs under Prime Agent.
Give each account an `env_file` (or inline values) instead.

A missing `--config` file is only a warning: the server falls back to config
discovery and then to the environment. Set `NETCUP_MCP_CONFIG_REQUIRED=1` to
make it a hard error.

## Editing DNS records

`update_dns_records` replaces the whole record set. Read the current records
with `info_dns_records` first and pass back everything you want to keep.
Use `{"hostname": "@"}` for the zone apex, and set `deleterecord` to drop a
record.

## Testing

```sh
uv run pytest            # unit tests, no network
uv run pytest -m live    # hits the real API, needs credentials
```

The live tests skip automatically unless credentials are available, either
from the environment or from a config file (`NETCUP_CONFIG`, or
`netcup-mcp.toml` in the repo).

## Notes

- `updateDnsRecords` can skip DNSSEC records when `keepdnssecrecords` is set.
- DNSSEC changes can only be made once every 24 hours.
- Several tools need a reseller account. Those return a clear API error on
  a normal customer account.