Server data from the Official MCP Registry
Debug Kafka from an agent: find messages, measure consumer lag, unblock consumers, compare clusters.
About
Debug Kafka from an agent: find messages, measure consumer lag, unblock consumers, compare clusters.
Security Report
Valid MCP server (4 strong, 1 medium validity signals). 5 known CVEs in dependencies (0 critical, 1 high severity) Imported from the Official MCP Registry.
7 files analyzed · 5 issues found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
Permissions Required
This plugin requests these system permissions. Most are normal for its category.
What You'll Need
Set these up before or after installing:
Environment variable: CONFIG_FILE
How to Install
Add this to your MCP configuration file:
{
"mcpServers": {
"io-github-denizgursoy-kafka-mcp": {
"env": {
"CONFIG_FILE": "your-config-file-here"
},
"args": [
"-y",
"kafka-mcp-docs"
],
"command": "npx"
}
}
}Documentation
View on GitHubFrom the project's GitHub README.
kafka-mcp
An MCP server that exposes Kafka debugging as tools an LLM can call. It speaks MCP over stdio by default, optionally serves streamable HTTP, and talks to Kafka with franz-go.
One process can connect to several Kafka clusters. In stdio mode, one endpoint is selected for the session. In HTTP mode, each endpoint has its own path, so a session is bound to one cluster by how it connects rather than by a parameter a caller could forget to send.
Install
Every release ships the same server four ways:
-
Binary:
kafka-mcp_<Os>_<arch>archives on the releases page. Putkafka-mcpon yourPATH. -
MCP Bundle:
kafka-mcp_<version>_<os>_<arch>.mcpbon the same page, for macOS on Apple Silicon, Linux (amd64, arm64) and Windows (amd64). Open it in a client that installs bundles, such as Claude Desktop. It asks for your configuration file and, when that file has several endpoints, which one to serve. Intel Macs use the binary or the image. -
Container image:
ghcr.io/denizgursoy/kafka-mcp:<tag>. It starts with--server; for stdio, mount the file and pass--server=false:docker run -i --rm \ --mount type=bind,src=$PWD/kafka-mcp.yaml,dst=/config/kafka-mcp.yaml,readonly \ -e CONFIG_FILE=/config/kafka-mcp.yaml \ ghcr.io/denizgursoy/kafka-mcp:latest --server=false --endpoint local -
MCP Registry: listed as
io.github.denizgursoy/kafka-mcpin the official registry, with the image and the bundles, so a client that reads the registry can install it from there.
How releases reach the registry and the other catalogs is in PUBLISHING.md.
Configuration
kafka-mcp.{toml,yaml,yml,json} in the working directory, ~/.config/kafka-mcp/ or /etc configures the server. The http block is used only with --server.
http:
address: ":8090"
base_path: /kafka-mcp # optional; prefixes every endpoint and /healthz
# public_url: https://kafka.example.com # optional; the address download links use
clusters:
prod:
brokers:
- kafka-1:9093
- kafka-2:9093
security:
tls:
enabled: true
ca_file: /etc/kafka/ca.pem
# cert_file: /etc/kafka/client.pem # optional mTLS; requires key_file
# key_file: /run/secrets/client.key
sasl:
- scram:
enabled: true
algorithm: SCRAM-SHA-256 # or SCRAM-SHA-512
user: kafka-mcp-readonly
pass: "{env:KAFKA_PASSWORD}" # or password_file: /run/secrets/kafka
preprod:
brokers: kafka-preprod:9093
endpoints:
prod-read:
cluster: prod
path: /mcp
description: Production investigation and debugging
read_only: true
prod-write:
cluster: prod
path: /mcp/rw
description: Approved production changes
tools:
copy_message: false
preprod:
cluster: preprod
path: /mcp/preprod
clusters owns Kafka connection details: brokers, TLS and SASL. endpoints
owns the policy and, in HTTP mode, the MCP route. Stdio selects an endpoint by
name and does not use its path. Several endpoints may reference one cluster, so
the example reuses one production connection at /kafka-mcp/mcp in read-only
mode and /kafka-mcp/mcp/rw in writable mode. Paths are exact: /mcp does not
capture /mcp/rw. description is optional and is reported by server_config
so a caller knows what the endpoint is intended for.
http.base_path prefixes endpoint paths and the liveness route. A leading or
trailing slash on an endpoint path is optional. Paths must be unique and may
not be /, /healthz, or contain a query or fragment. When path is omitted,
it defaults to /mcp/<endpoint-name>.
For compatibility, a file with no endpoints block still creates one endpoint
per cluster at /mcp/<cluster-name>. Existing cluster-level read_only and
tools values continue to apply as a lower bound during migration; an explicit
endpoint cannot widen them. New configurations should put both fields under
endpoints.
An endpoint may switch individual tools off, by name:
endpoints:
prod-read:
cluster: prod
path: /mcp
read_only: true
tools:
create_topic: false
commit_offset: false
Cross-cluster writes are granted per endpoint. copy_message,
produce_message and transform_messages may name a destination_cluster, and that cluster must be
listed in the calling endpoint's destinations:
endpoints:
prod-read:
cluster: prod
read_only: true
destinations: [preprod] # this session may copy into preprod, nowhere else
An endpoint with no destinations writes to no other cluster. A listed cluster
configured read_only still refuses, and listing the endpoint's own cluster is
an error, because that is governed by read_only. Names are checked at
startup. A file with no endpoints block keeps its old behaviour: each
cluster's endpoint lists every other cluster. Clusters that no endpoint serves
are invisible to list_clusters, compare_clusters and destination_cluster.
A tool the map does not mention stays exposed, so the file states only what it
withholds rather than relisting every tool and silently losing whatever is added
later. Names are exact and lowercase, as listed by server_config. They are
checked at startup: a name that is not a tool stops the
server, because a typo would leave the tool it was meant to withhold exposed.
server_config cannot be switched off, since it is how a session learns which
cluster and policy it reached and which tools that endpoint has. The tools
map only narrows an endpoint: read_only: true still withholds the writing
tools regardless of what the map says.
A minimal local file:
clusters:
local:
brokers: localhost:19092
schema_registry:
urls: ["http://localhost:18081"]
endpoints:
local:
cluster: local
path: /mcp/local
Run a custom configuration over stdio with
CONFIG_FILE=/path/to/config.yaml go run ./cmd/server. If it defines several
endpoints, select one with --endpoint <name>. Add --server to serve every
configured endpoint over HTTP instead; --endpoint is not used in HTTP mode.
Without CONFIG_FILE, chu discovers kafka-mcp.{toml,yaml,yml,json} first in
the working directory, then in the operating system's user config directory
(~/.config/kafka-mcp/ on Linux), and finally in /etc. It uses the first
matching file rather than merging files. Its standard loader order is defaults,
file, HTTP, then environment; environment overrides use the KAFKA_MCP_ prefix (for example,
KAFKA_MCP_HTTP_ADDRESS=:9000 or
KAFKA_MCP_HTTP_BASE_PATH=/kafka-mcp). Logging can be configured with
LOG_LEVEL and LOG_PRETTY.
At least one cluster with a broker is required. http.address defaults to
:8090 and http.base_path to the HTTP root.
The server never stores files. search_messages and transform_messages with
download, and produce_message with upload, work with files on the user's
own machine. See Files on the user's machine.
Over HTTP, set http.link_key (or http.auth_token) to the same value on
every instance behind one address. Links are built on the address each request
reached, honouring X-Forwarded-Proto and X-Forwarded-Host; set
http.public_url when a proxy hides the real address. The old output_dir
setting is refused at startup.
Files on the user's machine
Tools that save or send many messages work with files on the user's own machine, never on the server.
Over stdio the server is a process the agent started on that machine, so
download saves the file and upload reads it right away, relative to the
process's working directory (usually the project) or at an absolute path. The
response names the file in download.path or upload.path; a download writes
to a hidden partial file and only then takes its name, and never replaces an
existing file. Running the image over stdio needs the directory mounted, e.g.
docker run -i --rm -v "$PWD:/work" -w /work ….
Over HTTP a tool cannot write there itself: its result goes to the model,
not to disk. So the tools return a link and a curl command, and the agent
runs the command on the user's machine:
curl -fsS -o 'orders.csv.part' 'https://kafka.example.com/mcp/prod/files/Q2x…' && mv 'orders.csv.part' 'orders.csv'
curl -fsS -T 'orders-fixed.jsonl' 'https://kafka.example.com/mcp/prod/files/Zk9…'
Nothing is prepared ahead and nothing is stored: the data streams between that machine and Kafka while the command runs, so the message bodies never pass through the model either.
The link carries the request itself, sealed with AES-GCM under a key derived
from http.link_key, or http.auth_token when that is unset. The server keeps
nothing between handing a link out and its use, so any instance behind a
load balancer can serve a link another made, as long as they share the key.
Without either setting each process makes a random key and links open only
where they were made. Sealing means a link shows nothing of its script in
proxy logs, and cannot be changed or made up. When used, the request is checked
again with that instance's policy: a cluster turned read-only refuses an upload
made before.
A link expires after 10 minutes. Treat it as a secret until then: it needs no
bearer token, because the agent's curl does not carry the MCP client's
headers, and it can be used more than once until it expires. Running an upload
link twice sends the file twice. A failure before any data, such as a deleted
topic, is an HTTP error with the reason, which curl -f reports; a failure
after the first byte breaks the transfer, so curl -f fails instead of keeping
a short file. Links are served below the endpoint at <endpoint path>/files/
and checked for origin like the endpoint. A request too large to fit in a link
(about 6 KiB sealed, e.g. a very long script) is refused when the link is
made.
brokers accepts either one address as a scalar or several addresses as a YAML
list. Each address is passed to Kafka as a separate seed broker.
Keep secrets out of the file with {env:VAR} or password_file. Unknown fields
are ignored by chu.
TLS is configured with twmb/tlscfg, using its TLS 1.2 minimum and recommended
cipher suites. TLS uses the system trust store;
ca_file adds a custom CA. For mTLS, supply both cert_file and key_file.
security.sasl is a preference-ordered list: each entry enables either scram
or plain, or uses oauth as shown below. For PLAIN, use plain: {enabled: true, user: alice, pass: "{env:KAFKA_PASSWORD}"}.
Both accept optional zid (authorization identity) and password_file instead
of pass; SCRAM also accepts is_token: true for delegation tokens. Algorithms
are case-insensitive. Disabled entries are ignored; repeated mechanisms and
entries enabling multiple mechanisms are rejected. Fallback negotiates a
broker-supported mechanism; it does not retry invalid credentials.
All Kafka connections, including message-reading sessions, use these settings.
Legacy per-cluster tls and sasl: {mechanism, user, password, password_file}
remain supported, but cannot be combined with security on the same cluster.
For OAUTHBEARER client credentials, use this cluster security block:
security:
tls:
enabled: true
sasl:
- oauth:
enabled: true
token_url: https://identity.example.com/realms/apps/protocol/openid-connect/token
client_id: kafka-mcp
client_secret: "{env:KAFKA_CLIENT_SECRET}"
scopes: [kafka]
timeout: 10s
# zid: optional-authorization-id
# extensions: {tenant: example}
Alternatively, use oauth: {enabled: true, token: "{env:KAFKA_TOKEN}"} for a
static token. Configure exactly one mode: static token, client credentials, or
gcp. Client-credentials tokens are fetched
on authentication, cached per cluster across admin and reading connections,
and renewed near expiry using expires_in. Token requests use the current
authentication context and a timeout (default 10 seconds). Static tokens are
not renewed. The broker must support OAUTHBEARER and trust the identity provider.
Token endpoint error bodies are never reported, only their HTTP status, because
they can echo secrets.
oauth.proxy sends token requests through an http, https, socks5 or
socks5h proxy; when unset, HTTPS_PROXY, HTTP_PROXY and NO_PROXY apply.
Broker security.tls settings apply to Kafka connections only. The token
endpoint uses oauth.tls, which can also present a client certificate (RFC
8705). With a certificate, client_secret is optional: when empty, client_id
goes in the request body and the certificate authenticates the client.
- oauth:
enabled: true
token_url: https://identity.example.com/token
client_id: kafka-mcp
scopes: [kafka]
tls:
cert_file: /etc/certs/oauth/client-cert.pem
key_file: /etc/certs/oauth/client-key.pem
ca_file: /etc/certs/oauth/ca.pem # optional; system roots otherwise
oauth.gcp gets Google Cloud access tokens with an X.509 client certificate
(workload identity federation over mTLS), optionally impersonating a service
account. Use Google's external_account credential file; its
credential_source is ignored, because the certificate below is used instead.
- oauth:
enabled: true
gcp:
enabled: true
credentials_file: /etc/certs/gcp/wif-credentials.json
cert_file: /etc/certs/kafka/client-cert.pem
key_file: /etc/certs/kafka/client-key.pem
format: managed_kafka # Google Managed Service for Apache Kafka; default raw
# proxy: http://proxy:3128 # default: oauth.proxy, then the environment
# lifetime: 1h # impersonated token lifetime
# refresh_before: 5m # refresh this long before expiry
# scopes: [https://www.googleapis.com/auth/cloud-platform]
audience, token_url and service_account_impersonation_url may be set
directly and override the credential file. managed_kafka requires a service
account to impersonate. A failed refresh keeps using the cached token until it
actually expires.
In oauth.tls and oauth.gcp, cert, key, ca (and credentials for
gcp) may be given inline instead of as _file, not both; inline keys and
credentials accept {env:VAR}. Files are re-read on each token refresh, so a
rotated certificate applies without a restart.
insecure_skip_verify: true disables server certificate and hostname
verification. It is accepted under security.tls (brokers),
schema_registry.tls, oauth.tls (token endpoint) and oauth.gcp (Google STS
and impersonation), and defaults to false. It exposes the connection to
man-in-the-middle attacks: prefer ca_file, and do not use it in production.
A cluster that cannot be reached at startup is still served, and
list_clusters reports it as disconnected. One cluster being down must not
block debugging the others.
Message formats
Messages are decoded to JSON for every tool that shows or searches them, by the first rule that applies:
- The topic has a format in
topic_formats. - The bytes carry a Schema Registry header (Confluent wire format) and the
cluster has a
schema_registry: Avro, Protobuf (with message index and referenced schemas) or JSON Schema. - Otherwise JSON, then UTF-8 text, then base64.
clusters:
prod:
brokers: kafka-1:9093
schema_registry:
urls: ["https://schema-registry:8081"]
user: kafka-mcp # or bearer_token: "{env:SR_TOKEN}"
password: "{env:SR_PASSWORD}" # or password_file
# tls: {enabled: true, ca_file: /etc/sr/ca.pem}
topic_formats: # topics whose messages carry no schema id
"orders.*": # exact name or glob; longest match wins
value:
format: protobuf
proto_files: [shop/order.proto]
import_paths: [./protos] # or descriptor_set: ./orders.binpb
message_type: shop.Order
events:
value: {format: avro, schema_file: ./schemas/event.avsc}
metrics:
key: {format: text}
value: {format: msgpack}
Formats are avro, protobuf, json, msgpack, text and binary. Schema
files and .proto sources are loaded at startup, so a missing file or type stops
the server. A message that names a schema but cannot be decoded is returned as
base64 with decode_error saying why, never silently. JSON output has sorted
keys; Avro longs beyond 2^53 keep their exact value; unset Protobuf fields are
shown with their defaults.
Each rendered message reports format, schema_id and message_type for the
value, and key_format / key_schema_id when the key is not plain text.
server_config reports the endpoint name, exact path, description, policy and
the cluster it serves, and never the password. Its authentication and
sasl_user describe the first configured option; sasl_options lists all
configured identities in preference order, not the mechanism negotiated by an
individual broker connection. It also reports schema_registry URLs and
topic_formats.
Authentication and browser clients
http.auth_token makes every MCP request carry Authorization: Bearer <token>;
requests without it get 401. It accepts {env:VAR}. /healthz and CORS
preflights need no token. Without it, the endpoints have no authentication of
their own, which is safe only where nothing untrusted can reach the listener.
Set it with an environment variable, which keeps the secret out of the config file entirely:
export KAFKA_MCP_HTTP_AUTH_TOKEN="$(openssl rand -hex 32)"
CONFIG_FILE=kafka-mcp.yaml kafka-mcp --server
Every config key can be overridden this way: KAFKA_MCP_ followed by the key
path in upper case with _ between levels, so http.auth_token is
KAFKA_MCP_HTTP_AUTH_TOKEN. To keep the token in the file but the value in the
environment under a name of your choice, reference it instead:
http:
auth_token: "{env:MY_MCP_TOKEN}" # the server refuses to start if MY_MCP_TOKEN is unset
Clients then send the token on every request:
curl -X POST http://localhost:8090/mcp/local \
-H "Authorization: Bearer $KAFKA_MCP_HTTP_AUTH_TOKEN" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'
In the client configuration used under Connecting a client,
add the header to each remote entry. {env:...} is the client's own
substitution, so the token stays out of that file too:
{
"mcp": {
"kafka-local": {
"type": "remote",
"url": "http://localhost:8090/mcp/local",
"headers": { "Authorization": "Bearer {env:KAFKA_MCP_HTTP_AUTH_TOKEN}" }
}
}
}
In a container, pass it like any other secret:
docker run -e KAFKA_MCP_HTTP_AUTH_TOKEN="$TOKEN" \
-e KAFKA_MCP_HTTP_CORS_ALLOW_ORIGINS="https://mcp-client.example" \
-v "$PWD/kafka-mcp.yaml:/etc/kafka-mcp.yaml" -e CONFIG_FILE=/etc/kafka-mcp.yaml \
-p 8090:8090 kafka-mcp
A command-line client sends no Origin header and is unaffected by CORS. A
request that does carry an Origin is refused with 403 unless that origin is
in allow_origins. This happens before any tool runs, because CORS on its own
only hides the response from the page and the request itself would still
execute. No origin is allowed by default, so no web page the user visits
can drive the tools. To use a browser client, name its origin:
http:
address: ":8090"
cors:
allow_origins: ["https://mcp-client.example"]
allow_methods: [GET, POST, DELETE, OPTIONS]
allow_headers: [content-type, accept, authorization, cache-control, last-event-id, mcp-session-id, mcp-protocol-version]
expose_headers: [Mcp-Session-Id]
allow_private_network: true
max_age: 600
The same list from the environment is comma-separated:
KAFKA_MCP_HTTP_CORS_ALLOW_ORIGINS="https://a.example,https://b.example".
Setting it to * restores the old allow-everything behaviour.
http.cors is ada's CORS middleware configuration, read straight from the
file, so every option that middleware has is available here. Each key is
optional and keeps its own default, so setting allow_origins alone does not
drop the rest. The defaults are the values above with no allow_origins.
allow_origins: ["*"] restores the old allow-everything behaviour.
Three of them are load-bearing for the MCP transport. allow_methods needs
GET, POST and DELETE: requests are posted, the event stream is a GET,
and a client ends its session with DELETE. allow_headers needs
mcp-session-id and mcp-protocol-version, which the client sends from the
second request onwards, and a header missing there fails the whole preflight
rather than being dropped. Mcp-Session-Id must stay in expose_headers: the
session id arrives on the initialize response, and a page that cannot read
it cannot make a second call.
allow_private_network answers Chrome's Private Network Access preflight,
which a page on a public address must pass before it may reach a server on a
private or loopback address; it defaults to on, and is granted only on a
preflight the rest of the policy already allowed. Setting allow_credentials
together with a wildcard allow_origins is refused by the middleware at
startup unless unsafe_wildcard_origin_with_allow_credentials is also set,
which it should not be.
An allowed origin can drive every tool with the server's Kafka credentials, so
list only pages that should have that power, set auth_token (a browser client
then has to send it), and set read_only: true on endpoints that should not
write. A less obvious path such as /mcp/rw is not authentication.
Embedding the tools in a Go application
An application that already holds Kafka clients can register these tools on
its own MCP server with github.com/denizgursoy/kafka-mcp/pkg/kafkamcp. That
package loads no configuration, opens no listener and dials nothing. The host
keeps routing, authentication and transport; the tools, results, read-only
refusals and audit records are the same as the executable's.
clusters, err := kafkamcp.NewClusters(
kafkamcp.Connection{Name: "stage", Kafka: stageClient, NewReader: newStageReader},
kafkamcp.Connection{Name: "prod", Kafka: prodClient, NewReader: newProdReader, ReadOnly: true},
)
server := mcp.NewServer(&mcp.Implementation{Name: "my-app-stage"}, nil)
err = clusters.Register(server, "stage", kafkamcp.Options{Destinations: []string{"prod"}})
http.Handle("/mcp/stage", mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return server }, nil))
NewReadermust return a new client consuming the topic directly (kgo.ConsumePartitions) on every call. The tools close those clients; they never closeKafka,Admin,ManualProducer,BulkProducerorSchemaRegistry.AcquireReaderreplacesNewReaderwhen the host wants to bound or reuse readers: it lends a client with areleasefunction the tools call exactly once instead of closing it, and may wait for a free slot until the call's context ends. Lend clients withkgo.KeepControlRecords(); markers are skipped, and without them a scan ending on a transaction marker waits for data that never comes.BulkProduceris optional and writes the batches ofproduce_messageuploadandtransform_messages, so single writes onKafkado not queue behind them.ProtoMessageis optional and names the protobuf message a topic's key or value holds when written without Schema Registry framing, for hosts that already resolve their own.protofiles. Framed messages still go to the registry first, andtopic_formatsis not needed.Options.Loggerreceives the audit records (defaultslog.Default), andOptions.Usernames the person behind a call from its request headers, as the host's authenticating proxy established it.- Every cluster passed to
NewClusterscan be read bycompare_clustersandlist_clusters.copy_message,produce_messageandtransform_messageswrite to another cluster only when it is inOptions.Destinations, and aReadOnlyconnection refuses every write, whichever server it arrives from. Options.ReadOnlyhides and refuses writes to the registered cluster;Options.Toolsswitches tools off by name, and an unknown name is an error.server_configreports authentication ashost-managed, along withConnection.BrokersandConnection.Principal. Both are only reported.
Connecting a client
For the default stdio transport, configure the client to launch the binary. A configuration with one endpoint needs no arguments:
{
"mcp": {
"kafka-local": {
"type": "local",
"command": ["kafka-mcp"],
"environment": {"CONFIG_FILE": "/path/to/kafka-mcp.yaml"}
}
}
}
When the file contains several endpoints, add the endpoint selection to the
command, for example "command": ["kafka-mcp", "--endpoint", "prod-read"].
The process refuses to start without it so it cannot silently connect a session
to the wrong cluster or permission policy.
To use HTTP, start kafka-mcp --server and register one remote client entry per
endpoint:
{
"mcp": {
"kafka-local": {
"type": "remote",
"url": "http://localhost:8090/mcp/local"
},
"kafka-prod-read": {
"type": "remote",
"url": "http://localhost:8090/mcp",
"enabled": false
}
}
}
When the server sets http.auth_token, add
"headers": {"Authorization": "Bearer {env:KAFKA_MCP_HTTP_AUTH_TOKEN}"} to each
entry; see Authentication and browser clients.
The key becomes the tool prefix, so these appear as kafka-local_list_topics
and kafka-prod-read_list_topics. Name entries after both the cluster and the
endpoint policy: the prefix is the clearest signal of what a call can hit and
change. Use server_config to confirm rather than trusting the client-side
name.
Each enabled cluster costs context: these tools are roughly 8k tokens of definitions. Enable only what you need, and put production behind an agent:
{
"tools": { "kafka-prod*": false },
"agent": {
"kafka-prod": {
"description": "Debugging against production Kafka.",
"tools": { "kafka-prod*": true }
}
}
}
Permissions
Three layers, and only one of them is real security:
| Layer | Protects against | Real security? |
|---|---|---|
confirm: true on writes | An LLM changing things on one ambiguous request | No — a guardrail |
endpoint read_only: true | Accidental writes with the server's Kafka identity | No — anyone who can edit the config can turn it off |
| Kafka ACLs on the SASL principal | An unauthorised person | Yes — the broker decides |
What a read-only endpoint exposes
read_only: true does more than refuse a write: the endpoint does not list the
tools whose only purpose is to write. add_partitions, alter_topic_config,
commit_offset, create_topic, delete_consumer_group, delete_records and
delete_topic are absent from tools/list on a read-only endpoint, so a client never sees a tool it could not have used, and their
preview cannot describe a change this endpoint would never apply.
A writable endpoint can withhold individual tools too, with its tools map.
That is the same mechanism seen from the client: the tool is
not registered, so it is absent from tools/list and from server_config.
copy_message, produce_message and transform_messages stay, because
read_only protects the cluster being written to and the destination is chosen
per call. Copying a
message out of a read-only production cluster, or seeding a writable preprod
cluster from a protected session, is exactly what they are for. The destination
must be in the endpoint's destinations, and each refuses to write when the
destination is the read-only cluster itself.
Hiding a tool decides what is advertised, not what is permitted: both tools
still refuse at the point of mutation, so a registration mistake cannot turn
into a write. server_config reports the tools the endpoint actually exposes,
which is how a session can tell the two cases apart.
Giving two people different permissions
Kafka enforces permissions against the SASL principal, so two people running
the same server with different credentials get different rights. Adding
partitions requires ALTER on the topic:
# ali may read but not reshape topics
rpk acl create --allow-principal User:ali \
--operation read,describe --topic orders
# deniz may also add partitions
rpk acl create --allow-principal User:deniz \
--operation read,describe,alter --topic orders
Each points CONFIG_FILE at their own file, differing only in security.sasl[0].scram.user
and the password. When ali calls add_partitions, the broker refuses:
not authorized to add partitions to "orders": the broker refused this request.
Adding partitions requires ALTER permission on the topic for the principal
this server connects as
ali cannot bypass that by editing config or rebuilding the binary, because the
decision is made by Kafka rather than by this server. On a cluster without
ACLs, read_only: true is the available protection.
Audit logging
Every tool call is logged. One slog record per call, on the server's own log
stream, so nothing extra has to be configured:
{"level":"INFO","msg":"tool call","tool":"produce_message","outcome":"ok",
"duration_ms":12,"endpoint":"prod-write","cluster":"prod","read_only":false,
"principal":"kafka-mcp-rw","session":"QY7MZM...","client":"claude-code",
"client_version":"1.0.0","confirm":true,"item_count":1,"targets":"orders"}
The tools that change a cluster — add_partitions, alter_topic_config,
commit_offset, create_topic, copy_message, delete_consumer_group,
delete_records, delete_topic, produce_message, transform_messages — are
logged at INFO.
Everything else is logged at DEBUG, because reads are constant and change
nothing, so recording them at the same level would bury the writes among them.
Raise the log level to see them.
Two fields carry most of the weight. confirm separates a real write from a
preview, and targets names the topic, group, partition and offset each item
pointed at, so a record says which topic was touched rather than only that some
topic was. Tools without items record their top-level topic and destination, and
a produce_message upload records upload. A download records download.
Message content is never logged. produce_message, copy_message and
transform_messages carry
arbitrary payloads, and an audit log is usually readable by more people than the
data it describes, so keys, values and headers are left out. The audit code has
no field to unmarshal them into, so content cannot reach a log even by mistake.
None of the recorded identities is a person, and the server cannot make one up. What each actually means:
| Field | What it is |
|---|---|
principal | The Kafka credential this server connects as, which is what ACLs are enforced against. Everyone reaching the same endpoint shares it |
endpoint | Which endpoint policy allowed the call, and so which cluster was touched |
session | One MCP session, which groups a sequence of calls into one investigation |
client, client_version | The program that connected, as it identified itself at initialize. Self-reported and not verified |
user | Present only when an inbound bearer token established it. This server installs no token verifier, so it is absent today |
request_id | The HTTP request id, which joins a record to the access log |
For attribution to a person, give each person their own endpoint and SASL
credentials, as above: the principal in the record is then the answer to who
acted. A shared credential cannot be made to answer it.
Tools
Batch operations
describe_topic, sample_messages, get_message, get_schema,
consumer_lag, describe_consumer_group, open_transactions,
add_partitions, alter_topic_config, create_topic, commit_offset,
delete_consumer_group, delete_records, delete_topic, copy_message and
produce_message take their target only as an items array. There
is no single-target form: one operation is an items array of length one.
{"items": [{"topic": "orders"}]}
{"items": [{"topic": "orders"}, {"topic": "payments"}]}
Everything naming or shaping an operation lives on the item, so each field and
its description exist in exactly one place. What governs the whole call stays at
the top level, which in practice means confirm.
The message-heavy tools accept at most 500 items; lag, schema and administrative
tools accept at most 1000. For more messages than that, transform_messages
scans a topic in one call and produce_message sends any number from an
upload.
Every response is the same envelope. Results stay in input order, each entry
carrying index and either result or error, followed by succeeded,
failed, applied and atomic: false. An item's failure is data in the
response rather than an error for the call, so it never hides the items that
worked. Batch writes preview every item first, then apply the valid ones only
when the top-level confirm is true. They are not transactions: Kafka cannot
roll back a topic, partition, offset or produced message after a later item
fails. Duplicate write targets are refused before anything changes, except in
produce_message, where two identical items mean two messages rather than a
mistake.
list_clusters
Lists the clusters this server's endpoints serve, with whether each is
reachable, whether it is configured read_only, and whether this session may
write to it (writable: its own cluster unless the endpoint is read-only,
another only when listed in destinations). Takes no parameters. Available from every endpoint,
so a session can discover what copy_message, produce_message,
transform_messages and compare_clusters may target.
{"clusters": [
{"name": "prod", "connected": true, "read_only": true, "writable": false},
{"name": "preprod", "connected": true, "read_only": false, "writable": true}
], "count": 2}
connected is checked when you call, not recorded at startup, so a cluster
that has since gone down is reported honestly. Only the name, reachability and
writability are reported: broker addresses and credentials are deliberately
not, because this tool is reachable from every endpoint.
compare_clusters
Compares the topics of other clusters against the one this endpoint serves, and reports what differs. Use it to find what preproduction has that production does not, or to check whether two environments still match.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
items | object[] | yes | 1 to 1000 clusters to compare against |
Item fields:
| Field | Type | Required | Meaning |
|---|---|---|---|
cluster | string | yes | The other cluster. Use list_clusters for valid names |
search | string | no | Case-insensitive substring a topic name must contain |
include_internal | bool | no | Default false: internal topics are excluded |
{"here": "preprod", "there": "prod",
"here_cluster": {"name": "preprod", "brokers": 1, "topics": 12},
"there_cluster": {"name": "prod", "brokers": 3, "topics": 11},
"only_here": [{"topic": "orders-v2", "partitions": 6, "replication_factor": 1,
"configs": {"retention.ms": "604800000"}}],
"only_there": [],
"differing": [{"topic": "orders", "differences": ["partitions"],
"here": {"partitions": 1}, "there": {"partitions": 12}}],
"in_both": 11}
only_here and only_there name the direction, which is decided by the
endpoint you call: "here" is always the cluster this endpoint serves. Entries
carry the partition count, replication factor and explicitly-set configs of the
cluster that has the topic, so they can be passed straight to create_topic.
This tool creates and changes nothing. To create the missing topics, hand
the chosen entries to create_topic, which previews them against the broker
first and warns that a partition count can never be reduced.
differing is usually the more valuable half: a topic that exists on both sides
with a different partition count or retention.ms is the common reason a bug
reproduces in one environment and not the other. Only configs a topic sets for
itself are compared, because two clusters may carry different broker defaults
and comparing inherited values would report every topic as different.
A difference is not necessarily a mistake. A topic missing from production is often deliberate, so the report says what differs, never what is correct.
Topic listings come from the Kafka client's metadata cache, which is a few
seconds old, so a topic created moments earlier may still appear in
only_there. Repeat the comparison rather than creating it twice.
list_topics
Lists the topics on the cluster, sorted by name, each with its partition count, replication factor, the configs it sets for itself, and its size on disk.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
script | string | no | JavaScript predicate deciding whether a topic is listed |
timeout_seconds | int | no | Limit for evaluating the script. Default 30, at most 3600 |
The predicate sees:
Documentation truncated — see the full README on GitHub.
Reviews
No reviews yet
Be the first to review this server!
More Data & Analytics MCP Servers
Toleno
Freeby Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
MCP Marketplace
Freeby mcp-marketplace · Developer Tools
Search and install MCP servers from inside your AI client.
mcp-creator-python
Freeby mcp-marketplace · Developer Tools
Create, build, and publish Python MCP servers to PyPI — conversationally.
by Lharries · Communication
Read, search, and send WhatsApp messages through your AI assistant
FinAgent
Freeby mcp-marketplace · Finance
Free stock data and market news for any MCP-compatible AI assistant.
Memory
Freeby Modelcontextprotocol · Productivity
Knowledge graph-based persistent memory across sessions
