From 2a09e8d26f8375400e79f4370cc6c55c23b0b5da Mon Sep 17 00:00:00 2001 From: caballeto Date: Thu, 11 Jun 2026 16:59:14 +0200 Subject: [PATCH 1/4] feat: add get_api_key tool Adds the api-keys.get capability to the MCP server, mirroring the GET /api/v1/api-keys/{id} endpoint that the Python and JS SDKs already expose. Closes the capability-parity gap flagged by the monorepo parity check (mcp-server was the only surface missing api-keys.get). Co-authored-by: Cursor --- src/devhelm_mcp/tools/api_keys.py | 8 ++++++++ tests/test_tools.py | 1 + 2 files changed, 9 insertions(+) diff --git a/src/devhelm_mcp/tools/api_keys.py b/src/devhelm_mcp/tools/api_keys.py index fb826ba..040c3a8 100644 --- a/src/devhelm_mcp/tools/api_keys.py +++ b/src/devhelm_mcp/tools/api_keys.py @@ -24,6 +24,14 @@ def list_api_keys(api_token: str | None = None) -> ToolResult: except DevhelmError as e: raise_tool_error(e) + @mcp.tool() + def get_api_key(key_id: str, api_token: str | None = None) -> ToolResult: + """Get a single API key's metadata by id. The secret value is never returned.""" + try: + return serialize(get_client(api_token).api_keys.get(key_id)) + except DevhelmError as e: + raise_tool_error(e) + @mcp.tool() def create_api_key( body: CreateApiKeyRequest, api_token: str | None = None diff --git a/tests/test_tools.py b/tests/test_tools.py index 4b7a806..21877c2 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -75,6 +75,7 @@ "delete_webhook", "test_webhook", "list_api_keys", + "get_api_key", "create_api_key", "revoke_api_key", "delete_api_key", From 2bfe1ac930999a512e793a3495ee71e3642bd8f3 Mon Sep 17 00:00:00 2001 From: caballeto Date: Thu, 11 Jun 2026 18:30:32 +0200 Subject: [PATCH 2/4] fix(test): apply schema strip in registered_tools fixture The v0.7.2 hotfix moved _strip_internal_schema_fields() out of import time (it crashed Uvicorn via asyncio.run() inside a running loop) into the HTTP lifespan and stdio entrypoint. The test fixture kept calling mcp.list_tools() directly, so it asserted against the pre-strip schema and test_api_token_hidden_from_input_schema failed on a clean checkout. Apply the strip in the fixture exactly as production does, so the hidden-field assertions reflect the real wire schema. Co-authored-by: Cursor --- tests/test_tools.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 21877c2..50d078e 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -11,7 +11,7 @@ import pytest -from devhelm_mcp.server import mcp +from devhelm_mcp.server import _strip_internal_schema_fields, mcp RegisteredTools = dict[str, Any] @@ -142,7 +142,16 @@ @pytest.fixture(scope="module") def registered_tools() -> RegisteredTools: - tools = asyncio.run(mcp.list_tools()) + # Mirror production startup: the schema strip (api_token / managedBy) is + # applied in the HTTP lifespan and stdio entrypoint, NOT at import time + # (that was moved out in the v0.7.2 hotfix to avoid an asyncio.run() crash + # under Uvicorn). The fixture must apply it too, otherwise it asserts + # against the pre-strip schema and the hidden-field tests fail. + async def _build() -> list[Any]: + await _strip_internal_schema_fields() + return await mcp.list_tools() + + tools = asyncio.run(_build()) return {t.name: t for t in tools} From c09cc0d1767a1f89cccada1873599e89d99ffd2c Mon Sep 17 00:00:00 2001 From: caballeto Date: Fri, 12 Jun 2026 08:15:03 +0200 Subject: [PATCH 3/4] test: coerce list_tools() result to list for fixture return type Co-authored-by: Cursor --- tests/test_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 50d078e..c09259f 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -149,7 +149,7 @@ def registered_tools() -> RegisteredTools: # against the pre-strip schema and the hidden-field tests fail. async def _build() -> list[Any]: await _strip_internal_schema_fields() - return await mcp.list_tools() + return list(await mcp.list_tools()) tools = asyncio.run(_build()) return {t.name: t for t in tools} From 80f242b9b57fe0a3b1bb8b03dd227c8295d06a26 Mon Sep 17 00:00:00 2001 From: caballeto Date: Fri, 12 Jun 2026 14:52:28 +0200 Subject: [PATCH 4/4] chore: require devhelm>=1.4.0 for api_keys.get The get_api_key tool calls client.api_keys.get, which ships in devhelm SDK 1.4.0 (sdk-python#41, released). Bump the floor + refresh the lock so typecheck resolves the attribute and the installed SDK has the method. Co-authored-by: Cursor --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2fff42e..a1dc0e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ # `client.services` catalog resource and the extended # `client.dependencies` (component-level track + alert sensitivity) # that the services/dependencies tool modules call directly. - "devhelm>=1.3.0", + "devhelm>=1.4.0", "fastmcp>=3.2.3,<4", ] diff --git a/uv.lock b/uv.lock index 8bb42ff..d455430 100644 --- a/uv.lock +++ b/uv.lock @@ -388,15 +388,15 @@ wheels = [ [[package]] name = "devhelm" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "pydantic", extra = ["email"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/2e/317082ad26945cdf815040ce48c45ecde7537ccae89d8011bc02840c9325/devhelm-1.3.0.tar.gz", hash = "sha256:fb270af6919bf6bc426de850b0bbd6251ec72a061f6d3170a79e2ead271283cb", size = 261987, upload-time = "2026-06-10T14:00:21.546Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/e2/9446a3c3751fa0b09462172f9f9208777aeb4bcb624eaa58a1a38eff42af/devhelm-1.4.0.tar.gz", hash = "sha256:739cd4eb8e35d7a42a63e961c62d4bc62470a70a83a67652a4e4037ce0788a98", size = 263937, upload-time = "2026-06-12T12:16:36.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/62/261bc0a6551d8098670e755e4a44a74ea8c032edda472364d9a6bd73992c/devhelm-1.3.0-py3-none-any.whl", hash = "sha256:19fac4faad7af71e384ebcb7afd63c8198ad03367c411f88769ac81a1bcbdea2", size = 88194, upload-time = "2026-06-10T14:00:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/1d/13/19cd23030b714a573e1d4f8d61ada95277c818c9915382b4edf3fb066a4b/devhelm-1.4.0-py3-none-any.whl", hash = "sha256:9b2b1dfb450f0871dc5806b462d41a3603932013148914c7447b278fd7995edc", size = 88701, upload-time = "2026-06-12T12:16:35.469Z" }, ] [[package]] @@ -419,7 +419,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "devhelm", specifier = ">=1.3.0" }, + { name = "devhelm", specifier = ">=1.4.0" }, { name = "fastmcp", specifier = ">=3.2.3,<4" }, ]