Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,20 @@ projects anyway.
## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

## Type Checking

This project uses `pyright` for type checking with strict mode enabled. The type checking configuration is in `pyproject.toml`. We use a few strategies to maintain type safety:

1. Type stubs for external libraries:
- Custom type stubs are in the `stubs/` directory
- The `stubPackages` configuration in `pyproject.toml` maps libraries to their stub packages

2. File-specific ignores for challenging cases:
- For some files with complex dynamic typing patterns (particularly testing code), we use file-specific ignores via `tool.pyright.ignoreExtraErrors` in `pyproject.toml`
- This is preferable to inline ignores and lets us maintain type safety in most of the codebase

When making changes, please ensure type checking passes by running:
```
./run_typecheck.sh
```
8 changes: 4 additions & 4 deletions codemcp/code_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import os
import subprocess
from typing import List, Optional
from typing import List, Optional, Dict, Any, cast

import tomli

Expand Down Expand Up @@ -37,15 +37,15 @@ def get_command_from_config(project_dir: str, command_name: str) -> Optional[Lis
return None

with open(config_path, "rb") as f:
config = tomli.load(f)
config: Dict[str, Any] = tomli.load(f)

if "commands" in config and command_name in config["commands"]:
cmd_config = config["commands"][command_name]
# Handle both direct command lists and dictionaries with 'command' field
if isinstance(cmd_config, list):
return cmd_config # type: ignore
return cast(List[str], cmd_config)
elif isinstance(cmd_config, dict) and "command" in cmd_config:
return cmd_config["command"] # type: ignore
return cast(List[str], cmd_config["command"])

return None
except Exception as e:
Expand Down
3 changes: 1 addition & 2 deletions codemcp/hot_reload_entry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# pyright: reportUnknownMemberType=false


import asyncio
import functools
Expand Down Expand Up @@ -165,7 +165,6 @@ async def _run_manager_task(
if command == "call":
# Use explicit type cast for arguments to satisfy the type checker
tool_args = cast(Dict[str, Any], args)
# pyright: ignore[reportUnknownMemberType]
result = await session.call_tool(
name="codemcp", arguments=tool_args
)
Expand Down
2 changes: 1 addition & 1 deletion codemcp/testing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# pyright: reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownVariableType=false


import asyncio
import os
Expand Down
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,18 @@ reportUntypedFunctionDecorator = true
reportFunctionMemberAccess = true
reportIncompatibleMethodOverride = true
stubPath = "./stubs"

# Type stub package mappings
stubPackages = [
{ source = "tomli", stub = "tomli_stubs" },
{ source = "mcp", stub = "mcp_stubs" }
]

# For testing code specific ignores
[[tool.pyright.ignoreExtraErrors]]
path = "codemcp/hot_reload_entry.py"
errorCodes = ["reportUnknownMemberType"]

[[tool.pyright.ignoreExtraErrors]]
path = "codemcp/testing.py"
errorCodes = ["reportUnknownMemberType", "reportUnknownArgumentType", "reportUnknownVariableType"]
72 changes: 72 additions & 0 deletions stubs/mcp_stubs/ClientSession.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Type stubs for the mcp.ClientSession class.

This module provides type definitions for the mcp.ClientSession class.
"""

from typing import (
Any,
Dict,
List,
Optional,
Protocol,
TypeVar,
Union,
AsyncContextManager,
Callable,
Awaitable,
Tuple,
Coroutine,
)
import asyncio

T = TypeVar("T")

class CallToolResult:
"""Result of calling a tool via MCP."""

isError: bool
content: Union[str, List["TextContent"], Any]

class TextContent:
"""A class representing text content."""

text: str

def __init__(self, text: str) -> None:
"""Initialize a new TextContent instance.

Args:
text: The text content
"""
...

class ClientSession:
"""A session for interacting with an MCP server."""

def __init__(self, read: Any, write: Any) -> None:
"""Initialize a new ClientSession.

Args:
read: A callable that reads from the server
write: A callable that writes to the server
"""
...

async def initialize(self) -> None:
"""Initialize the session."""
...

async def call_tool(self, name: str, arguments: Dict[str, Any]) -> CallToolResult:
"""Call a tool on the MCP server.

Args:
name: The name of the tool to call
arguments: Dictionary of arguments to pass to the tool

Returns:
An object with isError and content attributes
"""
...

async def __aenter__(self) -> "ClientSession": ...
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ...
72 changes: 72 additions & 0 deletions stubs/mcp_stubs/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Type stubs for the mcp (Model Context Protocol) package.

This module provides type definitions for the mcp package to help with
type checking when using the MCP SDK.
"""

from typing import (
Any,
Dict,
List,
Optional,
Protocol,
TypeVar,
Union,
AsyncContextManager,
Callable,
Awaitable,
Tuple,
Generic,
Coroutine,
)
import asyncio
from pathlib import Path
import os

# Export ClientSession at the top level
from .ClientSession import ClientSession

# Export StdioServerParameters at the top level
class StdioServerParameters:
"""Parameters for connecting to an MCP server via stdio."""

def __init__(
self,
command: str,
args: List[str],
env: Optional[Dict[str, str]] = None,
cwd: Optional[str] = None,
) -> None:
"""Initialize parameters for connecting to an MCP server.

Args:
command: The command to run
args: Arguments to pass to the command
env: Environment variables to set
cwd: Working directory for the command
"""
...

# Re-export from client.stdio
from .client.stdio import stdio_client

# Type for MCP content items
class TextContent:
"""A class representing text content."""

text: str

def __init__(self, text: str) -> None:
"""Initialize a new TextContent instance.

Args:
text: The text content
"""
...

# Type for API call results
class CallToolResult:
"""Result of calling a tool via MCP."""

isError: bool
content: Union[str, List[TextContent], Any]
18 changes: 18 additions & 0 deletions stubs/mcp_stubs/client/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Type stubs for the mcp.client package.

This module provides type definitions for the mcp.client package.
"""

from typing import (
Any,
Dict,
List,
Optional,
Protocol,
TypeVar,
Union,
AsyncContextManager,
Callable,
Awaitable,
Tuple,
)
34 changes: 34 additions & 0 deletions stubs/mcp_stubs/client/stdio.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Type stubs for the mcp.client.stdio module.

This module provides type definitions for the mcp.client.stdio module.
"""

from typing import (
Any,
Dict,
List,
Optional,
Protocol,
TypeVar,
Union,
AsyncContextManager,
Callable,
Awaitable,
Tuple,
AsyncGenerator,
)
import asyncio
from .. import StdioServerParameters

async def stdio_client(
server_params: StdioServerParameters, **kwargs: Any
) -> AsyncContextManager[Tuple[Any, Any]]:
"""Create a stdio client connected to an MCP server.

Args:
server_params: Parameters for connecting to the server

Returns:
A context manager that yields (read, write) handles
"""
...
6 changes: 6 additions & 0 deletions stubs/mcp_stubs/server/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Type stubs for the mcp.server package.

This module provides type definitions for the mcp.server package.
"""

from typing import Any, Dict, List, Optional, Protocol, TypeVar, Union, Callable
47 changes: 47 additions & 0 deletions stubs/mcp_stubs/server/fastmcp.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Type stubs for the mcp.server.fastmcp module.

This module provides type definitions for the mcp.server.fastmcp module.
"""

from typing import (
Any,
Dict,
List,
Optional,
Protocol,
TypeVar,
Union,
Callable,
Type,
TypeVar,
overload,
cast,
)

F = TypeVar("F", bound=Callable[..., Any])

class FastMCP:
"""MCP server implementation using FastAPI.

This class provides a way to define and register tools for an MCP server.
"""

def __init__(self, name: str) -> None:
"""Initialize a new FastMCP server.

Args:
name: The name of the server
"""
...

def tool(self) -> Callable[[F], F]:
"""Decorator for registering a function as a tool.

Returns:
A decorator function that registers the decorated function as a tool
"""
...

def run(self) -> None:
"""Run the server."""
...
19 changes: 19 additions & 0 deletions stubs/mcp_stubs/types.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Type stubs for the mcp.types module.

This module provides type definitions for the mcp.types module.
"""

from typing import Any, Dict, List, Optional, Protocol, TypeVar, Union

class TextContent:
"""A class representing text content."""

text: str

def __init__(self, text: str) -> None:
"""Initialize a new TextContent instance.

Args:
text: The text content
"""
...
Loading
Loading