fix: mypy errors silently dropped during CI (#832)

Resolves
https://linear.app/arcadedev/issue/TOO-788/mypy-failures-are-silently-dropped-during-arcade-mcp-ci

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: primarily CI/Makefile behavior and type-annotation tweaks;
functional logic is unchanged aside from stricter failure propagation in
`make check`.
> 
> **Overview**
> **Stops CI from silently ignoring mypy failures.** The `make check`
target now runs `mypy` across `libs/arcade*/` and exits non-zero if any
package fails, reporting the failed libs.
> 
> Separately tightens typing to satisfy `mypy` (removing `type: ignore`
on OAuth helpers, adding `cast()`/`Any` annotations for JSON response
shapes and subprocess kwargs, and handling non-`str` `server_address`
hosts), and bumps patch versions for `arcade-mcp` and
`arcade-mcp-server`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e79575b13a2d03adf3548104a0064c643f1e21b1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Eric Gustin 2026-04-28 13:25:44 -07:00 committed by GitHub
parent be52f07930
commit cbe68462df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 27 additions and 20 deletions

View file

@ -10,11 +10,15 @@ install: ## Install the uv environment and all packages with dependencies
check: ## Run code quality tools. check: ## Run code quality tools.
@echo "🚀 Linting code: Running pre-commit" @echo "🚀 Linting code: Running pre-commit"
@uv run pre-commit run -a @uv run pre-commit run -a
@echo "🚀 Static type checking: Running mypy on libs" @echo "🚀 Static type checking: Running mypy on libs"
@for lib in libs/arcade*/ ; do \ @failed_libs=""; for lib in libs/arcade*/ ; do \
echo "🔍 Type checking $$lib"; \ echo "🔍 Type checking $$lib"; \
(cd $$lib && uv run mypy . --exclude tests || true); \ (cd $$lib && uv run mypy . --exclude tests) || failed_libs="$$failed_libs $$lib"; \
done done; \
if [ -n "$$failed_libs" ]; then \
echo "❌ mypy failed in:$$failed_libs"; \
exit 1; \
fi
.PHONY: check-libs .PHONY: check-libs
check-libs: ## Run code quality tools for each lib package check-libs: ## Run code quality tools for each lib package

View file

@ -75,7 +75,7 @@ def _get_default_oauth_timeout_seconds() -> int:
DEFAULT_OAUTH_TIMEOUT_SECONDS = _get_default_oauth_timeout_seconds() DEFAULT_OAUTH_TIMEOUT_SECONDS = _get_default_oauth_timeout_seconds()
def create_oauth_client(cli_config: CLIConfig) -> OAuth2Client: # type: ignore[no-any-unimported] def create_oauth_client(cli_config: CLIConfig) -> OAuth2Client:
""" """
Create an authlib OAuth2Client configured for the CLI. Create an authlib OAuth2Client configured for the CLI.
@ -92,7 +92,7 @@ def create_oauth_client(cli_config: CLIConfig) -> OAuth2Client: # type: ignore[
) )
def generate_authorization_url( # type: ignore[no-any-unimported] def generate_authorization_url(
client: OAuth2Client, client: OAuth2Client,
cli_config: CLIConfig, cli_config: CLIConfig,
redirect_uri: str, redirect_uri: str,
@ -123,7 +123,7 @@ def generate_authorization_url( # type: ignore[no-any-unimported]
return url, code_verifier return url, code_verifier
def exchange_code_for_tokens( # type: ignore[no-any-unimported] def exchange_code_for_tokens(
client: OAuth2Client, client: OAuth2Client,
code: str, code: str,
redirect_uri: str, redirect_uri: str,
@ -321,7 +321,7 @@ class _LoopbackHTTPServer(HTTPServer):
def server_bind(self) -> None: def server_bind(self) -> None:
socketserver.TCPServer.server_bind(self) socketserver.TCPServer.server_bind(self)
host, port = self.server_address[:2] host, port = self.server_address[:2]
self.server_name = host self.server_name = host if isinstance(host, str) else host.decode()
self.server_port = port self.server_port = port

View file

@ -6,6 +6,7 @@ import json as _json
import logging import logging
import time import time
from pathlib import Path from pathlib import Path
from typing import Any, cast
import httpx import httpx
from arcade_core.constants import PROD_COORDINATOR_HOST, PROD_ENGINE_HOST from arcade_core.constants import PROD_COORDINATOR_HOST, PROD_ENGINE_HOST
@ -55,7 +56,7 @@ def _read_cache(debug: bool = False) -> dict[str, list[str]] | None:
return None return None
if debug: if debug:
console.print(f" [dim]Using cached tool catalog ({age:.0f}s old)[/dim]") console.print(f" [dim]Using cached tool catalog ({age:.0f}s old)[/dim]")
return data.get("toolkits", {}) return cast("dict[str, list[str]]", data.get("toolkits", {}))
except Exception: except Exception:
return None return None
@ -318,7 +319,7 @@ def list_gateways(
return [] return []
data = resp.json() data = resp.json()
return data.get("items", []) return cast("list[dict[Any, Any]]", data.get("items", []))
def find_matching_gateway( def find_matching_gateway(
@ -401,13 +402,13 @@ def create_gateway(
if resp.status_code not in (200, 201): if resp.status_code not in (200, 201):
raise RuntimeError(f"Failed to create gateway ({resp.status_code}): {resp.text}") raise RuntimeError(f"Failed to create gateway ({resp.status_code}): {resp.text}")
data = resp.json() data: dict[Any, Any] = resp.json()
# The API may return the gateway directly or wrapped in a list/items envelope # The API may return the gateway directly or wrapped in a list/items envelope
if "slug" in data: if "slug" in data:
return data return data
if data.get("items"): if data.get("items"):
return data["items"][0] return cast("dict[Any, Any]", data["items"][0])
if "id" in data: if "id" in data:
return data return data
@ -695,13 +696,13 @@ def _resolve_gateway_slug(
if gw.get("slug", "").lower() == input_lower: if gw.get("slug", "").lower() == input_lower:
if debug: if debug:
console.print(f" [dim]Matched by slug: {gw['slug']}[/dim]") console.print(f" [dim]Matched by slug: {gw['slug']}[/dim]")
return gw["slug"] return cast("str", gw["slug"])
for gw in gateways: for gw in gateways:
if gw.get("name", "").lower() == input_lower: if gw.get("name", "").lower() == input_lower:
slug = gw["slug"] slug = gw["slug"]
if debug: if debug:
console.print(f" [dim]Matched by name '{gw['name']}' -> slug: {slug}[/dim]") console.print(f" [dim]Matched by name '{gw['name']}' -> slug: {slug}[/dim]")
return slug return cast("str", slug)
if debug: if debug:
available = [f"{g.get('name')} ({g.get('slug')})" for g in gateways] available = [f"{g.get('name')} ({g.get('slug')})" for g in gateways]
console.print(f" [dim]No match for '{user_input}', available: {available}[/dim]") console.print(f" [dim]No match for '{user_input}', available: {available}[/dim]")

View file

@ -4,7 +4,7 @@ import os
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Any, Optional
import click import click
import typer import typer
@ -334,7 +334,7 @@ def mcp(
# window from appearing (e.g. when an MCP client spawns this # window from appearing (e.g. when an MCP client spawns this
# command without an attached console). The child process still # command without an attached console). The child process still
# inherits stdin/stdout/stderr for stdio transport communication. # inherits stdin/stdout/stderr for stdio transport communication.
run_kwargs: dict[str, object] = {"check": False} run_kwargs: dict[str, Any] = {"check": False}
creation_flags = get_windows_no_window_creationflags() creation_flags = get_windows_no_window_creationflags()
if creation_flags: if creation_flags:
run_kwargs["creationflags"] = creation_flags run_kwargs["creationflags"] = creation_flags

View file

@ -184,7 +184,9 @@ def build_input_schema_from_definition(definition: ToolDefinition) -> dict[str,
if getattr(definition, "input", None) and getattr(definition.input, "parameters", None): if getattr(definition, "input", None) and getattr(definition.input, "parameters", None):
for param in definition.input.parameters: for param in definition.input.parameters:
val_schema = getattr(param, "value_schema", None) val_schema = getattr(param, "value_schema", None)
schema = _value_schema_to_json_schema(val_schema) if val_schema else {"type": "string"} schema: dict[str, Any] = (
_value_schema_to_json_schema(val_schema) if val_schema else {"type": "string"}
)
if getattr(param, "description", None): if getattr(param, "description", None):
schema["description"] = param.description schema["description"] = param.description

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade-mcp-server" name = "arcade-mcp-server"
version = "1.21.0" version = "1.21.1"
description = "Model Context Protocol (MCP) server framework for Arcade.dev" description = "Model Context Protocol (MCP) server framework for Arcade.dev"
readme = "README.md" readme = "README.md"
authors = [{ name = "Arcade.dev" }] authors = [{ name = "Arcade.dev" }]

View file

@ -1,6 +1,6 @@
[project] [project]
name = "arcade-mcp" name = "arcade-mcp"
version = "1.14.0" version = "1.14.1"
description = "Arcade.dev - Tool Calling platform for Agents" description = "Arcade.dev - Tool Calling platform for Agents"
readme = "README.md" readme = "README.md"
license = { file = "LICENSE" } license = { file = "LICENSE" }