{"slug": "one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one", "title": "One Iceberg MCP Server, Seven Catalogs: What It Takes to Reach Each One", "summary": "A developer built a single MCP server for Apache Iceberg tables that reads from seven different Iceberg REST catalogs, switching catalogs via environment variables while keeping the server code unchanged. The server exposes four read-only tools — list_tables, describe_table, count_rows, and scan_table — and was tested against Polaris, BigLake, OneLake, Glue, S3 Tables, and Horizon, with each catalog requiring its own login and storage configuration. Failed catalog calls return text beginning with 'CATALOG ERROR' so an agent can report what it could not read.", "body_md": "This article provides a step by step guide to one MCP server for Apache Iceberg tables, pointed at seven Iceberg REST catalogs in turn. The server offers four read-only tools, and one environment variable decides which catalog it reads.\n\n[https://github.com/xbill9/lakehouse-iceberg-2026](https://github.com/xbill9/lakehouse-iceberg-2026)\n\nAn MCP server gives any MCP client, such as Claude Code or another coding agent, a fixed set of tools. This one has four, all reads:\n\n`iceberg_list_tables`` namespace.table` in the catalog`iceberg_describe_table`` iceberg_count_rows``iceberg_scan_table`\nEvery Iceberg REST catalog speaks the same protocol, so in principle the same server should work against all of them. This project checks that against seven catalogs:\n\nThe server is called directly over MCP, with no AI model involved, so each result depends only on the catalog and the server's settings. Measured on 2026-09-18 and 2026-09-19 (UTC), one run per catalog, reads only.\n\nThe strategy is an incremental step by step approach.\n\nFirst, the local Polaris catalog, which needs only Docker. Then the MCP server, called directly to check all four tools. Then one catalog at a time: its login, its storage package, and a run of the same four calls.\n\n`pyiceberg` 0.12.0 — this run used Python 3.14.7`gcloud`, `az`, `aws` or a Snowflake key pair\n\n``` bash\n$ git clone https://github.com/xbill9/lakehouse-iceberg-2026\n$ cd lakehouse-iceberg-2026/iceberg-conformance\n$ ./polaris-up.sh\n$ export POLARIS_CLIENT_ID=root POLARIS_CLIENT_SECRET=s3cr3t\n$ python3 seed_table.py --catalog apache-polaris\n```\n\nThat creates `probe_ns.probe_table`: 11 rows, partitioned by day, with four snapshots.\n\nThe server is `iceberg-mcp-hosts/servers/iceberg_mcp.py`. It speaks MCP over stdio as newline-delimited JSON-RPC and answers `initialize`, `tools/list`, `tools/call` and `ping`, with no MCP SDK.\n\nIt picks its catalog from two environment variables:\n\n``` bash\n$ ICEBERG_CATALOG=aws-glue ICEBERG_CATALOGS_FILE=catalogs.yaml python3 servers/iceberg_mcp.py\n```\n\n`catalogs.yaml` holds one entry per catalog: the URL, the warehouse, and how to log in. The server code is the same for every catalog.\n\n`sweep_catalogs.py` starts the server once per catalog and makes the same MCP calls each time: `initialize`, `tools/list`, then the four tools on the first table listed.\n\n``` bash\n$ cd ../iceberg-mcp-hosts\n$ python3 sweep_catalogs.py --only apache-polaris\napache-polaris       probe_ns.probe_table                 list_tables=ok(1.0s)  describe_table=ok(0.4s)  count_rows=ok(0.0s)  scan_table=ok(0.1s)\n```\n\nThe scan's reply shows what a client receives:\n\n```\nid | ts | payload | region\n0 | 2026-09-01 00:00:00+00:00 | row-0-0 | None\n2 | 2026-09-01 02:00:00+00:00 | row-0-2 | None\n3 | 2026-09-01 03:00:00+00:00 | row-0-3 | None\n\n3 of 11 row(s) shown, read from snapshot-id 1196292829914853564\nCOUNT: exactly 11 row(s) are in the table in snapshot-id 1196292829914853564. Exact, over the whole table.\nMIN and MAX of id over those 11 row(s): 0 and 23. Exact.\n```\n\nA failed catalog call comes back as text starting `CATALOG ERROR`, so an agent can still say what it could not read. The sweep counts that text as a failure.\n\nEach managed catalog needs its own login in `catalogs.yaml`. The server builds a PyIceberg `RestCatalog` from it:\n\n| catalog | login | how files are read | \n|---|---|---|\n| Polaris | OAuth2 client ID and secret | PyArrow, local `file:` | \n| BigLake | `gcloud` token,`x-goog-user-project` header | PyArrow, `gs://` | \n| OneLake | `az` token | fsspec with `adlfs` ,`abfss://` | \n| Glue | SigV4, service `glue` | PyArrow, `s3://` , local AWS login | \n| S3 Tables | SigV4, service `s3tables` | fsspec with `s3fs` , catalog-issued credential | \n| Horizon | Snowflake key-pair JWT | PyArrow, `s3://` , catalog-issued credential | \n\nS3 Tables issues a storage credential when asked with the `X-Iceberg-Access-Delegation: vended-credentials` header. Horizon returns one without being asked.\n\nListing, describing and counting read only catalog metadata, so they work with `pyiceberg` alone. Scanning reads data files, and three catalogs need packages that `pyiceberg` does not install by default. Without them the server answers three of the four tools and the scan fails:\n\n```\nCATALOG ERROR while scanning dbo.probe_table: ModuleNotFoundError: No module named 'adlfs'.\n\nCATALOG ERROR while scanning probe_ns.probe_table: ModuleNotFoundError: No module named 's3fs'.\n```\n\nAWS logins made with the newer `aws login` command need one more package before any call works:\n\n```\nCATALOG ERROR while listing tables: MissingDependencyException: Missing Dependency: Using the login credential provider requires an additional dependency. You will need to pip install \"botocore[crt]\" before proceeding.\nbash\n$ pip install adlfs s3fs \"botocore[crt]\"\n```\n\n| package | needed for | \n|---|---|\n| `adlfs` | OneLake scans | \n| `s3fs` | S3 Tables scans | \n| `botocore[crt]` | Glue and S3 Tables with an `aws login` session | \n\nOneLake's data files need an Azure storage credential as well as the catalog token. `DefaultAzureCredential` is the usual choice, and it tries the Azure VM metadata service before the `az` login. Off Azure, that attempt waits out its retries:\n\n```\n     917ms No environment configuration found.\n     921ms ManagedIdentityCredential will use IMDS\n  553989ms DefaultAzureCredential acquired a token from AzureCliCredential\nAzureCliCredential      0.6s\nDefaultAzureCredential  553.1s\n```\n\nThrough the MCP server, a three-row OneLake scan took 858.5 seconds with `DefaultAzureCredential` and 3.4 seconds with `AzureCliCredential`. Every other tool call was unaffected, because only the scan reads files.\n\n``` bash\n$ python3 sweep_catalogs.py --only apache-polaris,google-lakehouse,microsoft-onelake,aws-glue,aws-s3tables,snowflake-horizon\n$ python3 sweep_catalogs.py --report-only\ncatalog             measured (UTC)        table                   rows  partitioned     seconds: list describe count scan\napache-polaris      2026-09-18T23:58:37Z  probe_ns.probe_table      11  ts_day          1.0 0.5 0.0 0.1\naws-glue            2026-09-18T23:58:51Z  probe_ns.probe_table      11  ts_day          2.1 0.7 0.2 1.5\naws-s3tables        2026-09-18T23:58:57Z  probe_ns.probe_table      11  ts_day          2.2 0.3 0.2 2.0\ngoogle-lakehouse    2026-09-19T00:11:14Z  probe_ns.probe_table      11  ts_day          10.1 1.6 0.5 3.6\nmicrosoft-onelake   2026-09-18T23:58:45Z  dbo.probe_table            6  (unpartitioned) 2.5 0.2 0.2 3.2\nsnowflake-horizon   2026-09-18T23:59:02Z  PROBE_NS.PROBE_TABLE      12  ts_day          7.3 2.2 1.4 1.9\n```\n\n**All four tools work on all six catalogs.** Each one reported the same four tools from `tools/list`, and no call returned an error.\n\nThe first call on each catalog also builds the client and fetches a login token, which is why `list` is the slowest column. Each figure is a single call, so the times show scale only.\n\nThe tables were created separately for an [earlier article](https://dev.to/gde/seven-iceberg-rest-catalogs-what-they-declare-and-what-they-serve-40oj), and the server reports what each catalog holds:\n\n`dbo`, stores `id` as an optional `int`, and has 6 rows in an unpartitioned table` PROBE_NS.PROBE_TABLE`, and has 12 rows\nA client only has to use the names `iceberg_list_tables` returns. The server passes them back unchanged, so `dbo` and upper-case names need no special handling.\n\nLoading a Horizon table leaves S3 credentials on the client's file reader, although the catalog entry asks for none:\n\n```\nsnowflake-horizon\n  client.region\n  py-io-impl\n  s3.access-key-id\n  s3.secret-access-key\n  s3.session-token\n  s3.session-token-expires-at-ms\n```\n\nHorizon's catalog entry supplies no storage login, so its scan reads files with that credential. It also means a Horizon table load carries a live storage credential, so logs of that response need redacting.\n\nAny MCP client that starts stdio servers can run it. For Claude Code, a project `.mcp.json`:\n\n```\n{\n  \"mcpServers\": {\n    \"iceberg\": {\n      \"command\": \"python3\",\n      \"args\": [\"iceberg-mcp-hosts/servers/iceberg_mcp.py\"],\n      \"env\": {\n        \"ICEBERG_CATALOG\": \"aws-glue\",\n        \"ICEBERG_CATALOGS_FILE\": \"iceberg-conformance/catalogs.yaml\"\n      }\n    }\n  }\n}\n```\n\nTo reach a different catalog, change `ICEBERG_CATALOG`. The measurements above call the server directly and do not use this file.\n\n| catalog | four tools | extra package | login on the machine | \n|---|---|---|---|\n| Apache Polaris | 🟢 | — | OAuth2 client secret | \n| Google BigLake | 🟢 | — | `gcloud` | \n| Microsoft OneLake | 🟢 | `adlfs` | `az` | \n| AWS Glue | 🟢 | `botocore[crt]` with`aws login` | `aws` | \n| AWS S3 Tables | 🟢 | `s3fs` ,`botocore[crt]` with`aws login` | `aws` | \n| Snowflake Horizon | 🟢 | — | key pair | \n| Databricks Unity | not run |  |  | \n\nThe goal of this article was to point one Iceberg MCP server at seven catalogs and record what each one takes. The key to the solution was keeping the server code fixed, changing only the catalog entry, and calling the tools directly so that no model sits between the catalog and the result. The results were:\n\n`catalogs.yaml`\n`adlfs` for OneLake, `s3fs` for S3 Tables, and `botocore[crt]` for an `aws login` session; without them the metadata tools work and the scan fails`DefaultAzureCredential` took 553.1 seconds off Azure; `AzureCliCredential` took 0.6\nScope: `iceberg_mcp.py` 1.0.0 on `pyiceberg` 0.12.0, `pyarrow` 25.0.1, `s3fs` 2026.9.0, `adlfs` 2026.8.0, `botocore` 1.43.75 and `azure-identity` 1.25.3, on Python 3.14.7. Apache Polaris 1.7.0 in Docker with local file storage; the managed catalogs over the internet from one machine, AWS in `us-east-1`. One run per catalog on 2026-09-18 and 2026-09-19 (UTC); Unity not run. Reads only. The tools are checked for answering without error; the tables differ between catalogs, so values are reported and not compared. Managed catalogs do not report a version.\n\nThe strategy for pointing one Iceberg MCP server at seven catalogs was validated with an incremental step by step approach.", "url": "https://wpnews.pro/news/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one", "canonical_source": "https://dev.to/aws-builders/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one-jp5", "published_at": "2026-09-20 20:31:06+00:00", "updated_at": "2026-09-20 20:54:15.468817+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["Apache Iceberg", "Polaris", "BigLake", "OneLake", "AWS Glue", "S3 Tables", "Snowflake Horizon", "PyIceberg"], "alternates": {"html": "https://wpnews.pro/news/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one", "markdown": "https://wpnews.pro/news/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one.md", "text": "https://wpnews.pro/news/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one.txt", "jsonld": "https://wpnews.pro/news/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one.jsonld"}}