{
  "source_file": "0930",
  "source_platform": "Dev.to",
  "author": "xbill",
  "title_listed": "One Iceberg MCP Server, Seven Catalogs: What It Takes to Reach Each One",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "工程实践",
  "is_paid_flag": "否",
  "url": "https://dev.to/aws-builders/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one-jp5",
  "publish_date": "2026-09-20",
  "corpus_stage": "",
  "notes": "",
  "id": "538831be5e78",
  "collected_at": "2026-10-02T13:51:20",
  "title": "One Iceberg MCP Server, Seven Catalogs: What It Takes to Reach Each One - DEV Community",
  "body_markdown": "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.\nhttps://github.com/xbill9/lakehouse-iceberg-2026\nWhat is this project trying to Do?\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:\niceberg_list_tables\n— every\nnamespace.table\nin the catalog\niceberg_describe_table\n— columns, partitioning, snapshots and the metadata location\niceberg_count_rows\n— the exact row count from the snapshot summary\niceberg_scan_table\n— a few rows, plus an exact count, min and max\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:\nApache Polaris\n1.7.0, in Docker on the same machine\nGoogle BigLake\n,\nMicrosoft OneLake\n,\nAWS Glue\n,\nAWS S3 Tables\nand\nSnowflake Horizon\n, over the internet\nDatabricks Unity\n, not run: its trial account has ended\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.\nWhere do I start?\nThe strategy is an incremental step by step approach.\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.\nAt This Point You Should Have…\nDocker, for the local Polaris catalog\nPython 3.10+ with\npyiceberg\n0.12.0 — this run used Python 3.14.7\nFor each managed catalog you want to reach, an account with a table in it and a working command-line login:\ngcloud\n,\naz\n,\naws\nor a Snowflake key pair\nStep 1 — Start Polaris\n$\ngit clone https://github.com/xbill9/lakehouse-iceberg-2026\n$\ncd\nlakehouse-iceberg-2026/iceberg-conformance\n$\n./polaris-up.sh\n$\nexport\nPOLARIS_CLIENT_ID\n=\nroot\nPOLARIS_CLIENT_SECRET\n=\ns3cr3t\n$\npython3 seed_table.py\n--catalog\napache-polaris\nEnter fullscreen mode\nExit fullscreen mode\nThat creates\nprobe_ns.probe_table\n: 11 rows, partitioned by day, with four snapshots.\nStep 2 — Look at the Server\nThe server is\niceberg-mcp-hosts/servers/iceberg_mcp.py\n. It speaks MCP over stdio as newline-delimited JSON-RPC and answers\ninitialize\n,\ntools/list\n,\ntools/call\nand\nping\n, with no MCP SDK.\nIt picks its catalog from two environment variables:\n$\nICEBERG_CATALOG\n=\naws-glue\nICEBERG_CATALOGS_FILE\n=\ncatalogs.yaml python3 servers/iceberg_mcp.py\nEnter fullscreen mode\nExit fullscreen mode\ncatalogs.yaml\nholds one entry per catalog: the URL, the warehouse, and how to log in. The server code is the same for every catalog.\nStep 3 — Call It Without a Model\nsweep_catalogs.py\nstarts the server once per catalog and makes the same MCP calls each time:\ninitialize\n,\ntools/list\n, then the four tools on the first table listed.\n$\ncd\n../iceberg-mcp-hosts\n$\npython3 sweep_catalogs.py\n--only\napache-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)\nEnter fullscreen mode\nExit fullscreen mode\nThe scan's reply shows what a client receives:\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.\nEnter fullscreen mode\nExit fullscreen mode\nA failed catalog call comes back as text starting\nCATALOG ERROR\n, so an agent can still say what it could not read. The sweep counts that text as a failure.\nStep 4 — Add One Entry per Catalog\nEach managed catalog needs its own login in\ncatalogs.yaml\n. The server builds a PyIceberg\nRestCatalog\nfrom it:\ncatalog\nlogin\nhow files are read\nPolaris\nOAuth2 client ID and secret\nPyArrow, local\nfile:\nBigLake\ngcloud\ntoken,\nx-goog-user-project\nheader\nPyArrow,\ngs://\nOneLake\naz\ntoken\nfsspec with\nadlfs\n,\nabfss://\nGlue\nSigV4, service\nglue\nPyArrow,\ns3://\n, local AWS login\nS3 Tables\nSigV4, service\ns3tables\nfsspec with\ns3fs\n, catalog-issued credential\nHorizon\nSnowflake key-pair JWT\nPyArrow,\ns3://\n, catalog-issued credential\nS3 Tables issues a storage credential when asked with the\nX-Iceberg-Access-Delegation: vended-credentials\nheader. Horizon returns one without being asked.\nStep 5 — Install the Storage Packages\nListing, describing and counting read only catalog metadata, so they work with\npyiceberg\nalone. Scanning reads data files, and three catalogs need packages that\npyiceberg\ndoes not install by default. Without them the server answers three of the four tools and the scan fails:\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'.\nEnter fullscreen mode\nExit fullscreen mode\nAWS logins made with the newer\naws login\ncommand need one more package before any call works:\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.\nEnter fullscreen mode\nExit fullscreen mode\n$\npip\ninstall\nadlfs s3fs\n\"botocore[crt]\"\nEnter fullscreen mode\nExit fullscreen mode\npackage\nneeded for\nadlfs\nOneLake scans\ns3fs\nS3 Tables scans\nbotocore[crt]\nGlue and S3 Tables with an\naws login\nsession\n🔎 Tip: Use the\naz\nCredential Directly for OneLake\nOneLake's data files need an Azure storage credential as well as the catalog token.\nDefaultAzureCredential\nis the usual choice, and it tries the Azure VM metadata service before the\naz\nlogin. Off Azure, that attempt waits out its retries:\n917ms No environment configuration found.\n     921ms ManagedIdentityCredential will use IMDS\n  553989ms DefaultAzureCredential acquired a token from AzureCliCredential\nAzureCliCredential      0.6s\nDefaultAzureCredential  553.1s\nEnter fullscreen mode\nExit fullscreen mode\nThrough the MCP server, a three-row OneLake scan took 858.5 seconds with\nDefaultAzureCredential\nand 3.4 seconds with\nAzureCliCredential\n. Every other tool call was unaffected, because only the scan reads files.\nStep 6 — Run All Six\n$\npython3 sweep_catalogs.py\n--only\napache-polaris,google-lakehouse,microsoft-onelake,aws-glue,aws-s3tables,snowflake-horizon\n$\npython3 sweep_catalogs.py\n--report-only\nEnter fullscreen mode\nExit fullscreen mode\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\nEnter fullscreen mode\nExit fullscreen mode\nAll four tools work on all six catalogs.\nEach one reported the same four tools from\ntools/list\n, and no call returned an error.\nThe first call on each catalog also builds the client and fetches a login token, which is why\nlist\nis the slowest column. Each figure is a single call, so the times show scale only.\nStep 7 — Read What Came Back\nThe tables were created separately for an\nearlier article\n, and the server reports what each catalog holds:\nOneLake\nnames its namespace\ndbo\n, stores\nid\nas an optional\nint\n, and has 6 rows in an unpartitioned table\nHorizon\nreturns upper-case names,\nPROBE_NS.PROBE_TABLE\n, and has 12 rows\nThe other four\nhold the same 11-row table, partitioned by day\nA client only has to use the names\niceberg_list_tables\nreturns. The server passes them back unchanged, so\ndbo\nand upper-case names need no special handling.\n🔎 Tip: Horizon Hands Out Storage Credentials Unasked\nLoading a Horizon table leaves S3 credentials on the client's file reader, although the catalog entry asks for none:\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\nEnter fullscreen mode\nExit fullscreen mode\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.\nStep 8 — Use It From an MCP Client\nAny MCP client that starts stdio servers can run it. For Claude Code, a project\n.mcp.json\n:\n{\n\"mcpServers\"\n:\n{\n\"iceberg\"\n:\n{\n\"command\"\n:\n\"python3\"\n,\n\"args\"\n:\n[\n\"iceberg-mcp-hosts/servers/iceberg_mcp.py\"\n],\n\"env\"\n:\n{\n\"ICEBERG_CATALOG\"\n:\n\"aws-glue\"\n,\n\"ICEBERG_CATALOGS_FILE\"\n:\n\"iceberg-conformance/catalogs.yaml\"\n}\n}\n}\n}\nEnter fullscreen mode\nExit fullscreen mode\nTo reach a different catalog, change\nICEBERG_CATALOG\n. The measurements above call the server directly and do not use this file.\nCompare and Contrast\ncatalog\nfour tools\nextra package\nlogin on the machine\nApache Polaris\n🟢\n—\nOAuth2 client secret\nGoogle BigLake\n🟢\n—\ngcloud\nMicrosoft OneLake\n🟢\nadlfs\naz\nAWS Glue\n🟢\nbotocore[crt]\nwith\naws login\naws\nAWS S3 Tables\n🟢\ns3fs\n,\nbotocore[crt]\nwith\naws login\naws\nSnowflake Horizon\n🟢\n—\nkey pair\nDatabricks Unity\nnot run\nSummary\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🟢 All four tools work on all six catalogs run: Polaris, BigLake, OneLake, Glue, S3 Tables and Horizon\n🟢 The only change between catalogs is one environment variable and one entry in\ncatalogs.yaml\n⚠️ Three packages outside\npyiceberg\n's defaults are needed:\nadlfs\nfor OneLake,\ns3fs\nfor S3 Tables, and\nbotocore[crt]\nfor an\naws login\nsession; without them the metadata tools work and the scan fails\n⚠️\nDefaultAzureCredential\ntook 553.1 seconds off Azure;\nAzureCliCredential\ntook 0.6\n⚠️ Horizon returns S3 credentials with a table load, unasked\n❌ Databricks Unity was not run, because its trial account has ended\nScope:\niceberg_mcp.py\n1.0.0 on\npyiceberg\n0.12.0,\npyarrow\n25.0.1,\ns3fs\n2026.9.0,\nadlfs\n2026.8.0,\nbotocore\n1.43.75 and\nazure-identity\n1.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\nus-east-1\n. 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.\nThe strategy for pointing one Iceberg MCP server at seven catalogs was validated with an incremental step by step approach.\nReferences\nlakehouse-iceberg-2026 | GitHub\nModel Context Protocol specification\napache/iceberg-python\nApache Polaris\nSeven Iceberg REST Catalogs: What They Declare, and What They Serve\nFour Iceberg Tools, Three Agent Frameworks: What Ports, and What Doesn't",
  "metadata": {
    "title": "One Iceberg MCP Server, Seven Catalogs: What It Takes to Reach Each One - DEV Community",
    "description": "Step by step: one Python MCP server with four read-only Apache Iceberg tools, pointed at Polaris, BigLake, OneLake, Glue, S3 Tables and Horizon by changing one environment variable. All four tools work on all six. What changes per catalog is the login, the storage package, and one Azure credential that takes 553 seconds. Tagged with mcp, iceberg, python, dataengineering.",
    "keywords": "mcp, iceberg, python, dataengineering, software, coding, development, engineering, inclusive, community",
    "author": null,
    "og:type": "article",
    "og:url": "https://dev.to/aws-builders/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one-jp5",
    "og:title": "One Iceberg MCP Server, Seven Catalogs: What It Takes to Reach Each One",
    "og:description": "Step by step: one Python MCP server with four read-only Apache Iceberg tools, pointed at Polaris, BigLake, OneLake, Glue, S3 Tables and Horizon by changing one environment variable. All four tools work on all six. What changes per catalog is the login, the storage package, and one Azure credential that takes 553 seconds.",
    "og:site_name": "DEV Community",
    "og:image": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxbill9%2Flakehouse-iceberg-2026%2Fmain%2Fpapers%2Ficeberg-mcp-seven-catalogs%2Fcover.5c69639b.jpg",
    "twitter:site": "@thepracticaldev",
    "twitter:creator": "@",
    "twitter:title": "One Iceberg MCP Server, Seven Catalogs: What It Takes to Reach Each One",
    "twitter:description": "Step by step: one Python MCP server with four read-only Apache Iceberg tools, pointed at Polaris, BigLake, OneLake, Glue, S3 Tables and Horizon by changing one environment variable. All four tools work on all six. What changes per catalog is the login, the storage package, and one Azure credential that takes 553 seconds.",
    "twitter:card": "summary_large_image",
    "twitter:widgets:new-embed-design": "on",
    "twitter:image:src": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxbill9%2Flakehouse-iceberg-2026%2Fmain%2Fpapers%2Ficeberg-mcp-seven-catalogs%2Fcover.5c69639b.jpg"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/538831be5e78/code/body-000.txt",
      "chars": 237,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-001.txt",
      "chars": 94,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-002.txt",
      "chars": 222,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-003.txt",
      "chars": 397,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-004.txt",
      "chars": 189,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-005.txt",
      "chars": 219,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-006.txt",
      "chars": 41,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-007.txt",
      "chars": 233,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-008.txt",
      "chars": 167,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-009.txt",
      "chars": 747,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-010.txt",
      "chars": 141,
      "language": "text"
    },
    {
      "file": "attachments/538831be5e78/code/body-011.txt",
      "chars": 268,
      "language": "text"
    }
  ],
  "images": [],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://github.com/xbill9/lakehouse-iceberg-2026",
      "label": "https://github.com/xbill9/lakehouse-iceberg-2026",
      "type": "a"
    },
    {
      "href": "https://dev.to/gde/seven-iceberg-rest-catalogs-what-they-declare-and-what-they-serve-40oj",
      "label": "earlier article",
      "type": "a"
    },
    {
      "href": "https://modelcontextprotocol.io/specification",
      "label": "Model Context Protocol specification",
      "type": "a"
    },
    {
      "href": "https://github.com/apache/iceberg-python",
      "label": "apache/iceberg-python",
      "type": "a"
    },
    {
      "href": "https://polaris.apache.org/",
      "label": "Apache Polaris",
      "type": "a"
    },
    {
      "href": "https://dev.to/gde/four-iceberg-tools-three-agent-frameworks-what-ports-and-what-doesnt-a5g",
      "label": "Four Iceberg Tools, Three Agent Frameworks: What Ports, and What Doesn't",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:48:28Z",
  "source_url": "https://dev.to/aws-builders/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one-jp5",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 11658,
  "expected_body_images": 0,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/538831be5e78/pages/source.html",
  "local_html": "attachments/538831be5e78/pages/article.html",
  "body_base_path": "attachments/538831be5e78/pages/",
  "body_markdown_path": "attachments/538831be5e78/pages/article.md"
}