From 5aa1bfd38fabf1e71574a2a2bcd0fd18ec4c7e8c Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:09:36 -0500 Subject: [PATCH 01/10] Test fastmcp fix-stdio-cleanup-warning branch --- pyproject.toml | 8 ++++++-- uv.lock | 10 +++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 86623b0..26d641a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "rich>=14.2.0", "tomli>=2.3.0", "tomli-w>=1.2.0", - "fastmcp>=2.14.1", + "fastmcp @ git+https://github.com/jlowin/fastmcp@fix-stdio-cleanup-warning", "pydantic-ai>=1.39.0", "httpx>=0.27.0", ] @@ -42,7 +42,8 @@ env = [ "COLIN_DEFAULT_LLM_MODEL=test", ] filterwarnings = [ - "ignore::pytest.PytestUnraisableExceptionWarning:.*Event loop is closed.*", + # Testing if fastmcp fix-stdio-cleanup-warning branch fixes this + "error::pytest.PytestUnraisableExceptionWarning", ] [tool.ty.terminal] @@ -59,6 +60,9 @@ select = ["E", "F", "I", "UP"] requires = ["hatchling", "uv-dynamic-versioning"] build-backend = "hatchling.build" +[tool.hatch.metadata] +allow-direct-references = true + [tool.hatch.version] source = "uv-dynamic-versioning" diff --git a/uv.lock b/uv.lock index 7771973..77e7680 100644 --- a/uv.lock +++ b/uv.lock @@ -568,7 +568,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "cyclopts", specifier = ">=4.0.0" }, - { name = "fastmcp", specifier = ">=2.14.1" }, + { name = "fastmcp", git = "https://github.com/jlowin/fastmcp?rev=fix-stdio-cleanup-warning" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "jinja2", specifier = ">=3.1.0" }, { name = "pydantic", specifier = ">=2.0.0" }, @@ -862,8 +862,8 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.14.1" -source = { registry = "https://pypi.org/simple" } +version = "2.14.2.dev97+fe31e525" +source = { git = "https://github.com/jlowin/fastmcp?rev=fix-stdio-cleanup-warning#fe31e5252a078624b7c42105a823050f51a2d76f" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, @@ -882,10 +882,6 @@ dependencies = [ { name = "uvicorn" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/50/d38e4371bdc34e709f4731b1e882cb7bc50e51c1a224859d4cd381b3a79b/fastmcp-2.14.1.tar.gz", hash = "sha256:132725cbf77b68fa3c3d165eff0cfa47e40c1479457419e6a2cfda65bd84c8d6", size = 8263331, upload-time = "2025-12-15T02:26:27.102Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/82/72401d09dc27c27fdf72ad6c2fe331e553e3c3646e01b5ff16473191033d/fastmcp-2.14.1-py3-none-any.whl", hash = "sha256:fb3e365cc1d52573ab89caeba9944dd4b056149097be169bce428e011f0a57e5", size = 412176, upload-time = "2025-12-15T02:26:25.356Z" }, -] [[package]] name = "filelock" From 587c20be01d9263c4b5323fda5220f84a70bc09d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:20:26 -0500 Subject: [PATCH 02/10] Test fastmcp main branch instead --- pyproject.toml | 5 ++--- uv.lock | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 26d641a..7eb1de3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "rich>=14.2.0", "tomli>=2.3.0", "tomli-w>=1.2.0", - "fastmcp @ git+https://github.com/jlowin/fastmcp@fix-stdio-cleanup-warning", + "fastmcp @ git+https://github.com/jlowin/fastmcp@main", "pydantic-ai>=1.39.0", "httpx>=0.27.0", ] @@ -42,8 +42,7 @@ env = [ "COLIN_DEFAULT_LLM_MODEL=test", ] filterwarnings = [ - # Testing if fastmcp fix-stdio-cleanup-warning branch fixes this - "error::pytest.PytestUnraisableExceptionWarning", + "ignore::pytest.PytestUnraisableExceptionWarning:.*Event loop is closed.*", ] [tool.ty.terminal] diff --git a/uv.lock b/uv.lock index 77e7680..35847d3 100644 --- a/uv.lock +++ b/uv.lock @@ -568,7 +568,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "cyclopts", specifier = ">=4.0.0" }, - { name = "fastmcp", git = "https://github.com/jlowin/fastmcp?rev=fix-stdio-cleanup-warning" }, + { name = "fastmcp", git = "https://github.com/jlowin/fastmcp?rev=main" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "jinja2", specifier = ">=3.1.0" }, { name = "pydantic", specifier = ">=2.0.0" }, @@ -862,8 +862,8 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.14.2.dev97+fe31e525" -source = { git = "https://github.com/jlowin/fastmcp?rev=fix-stdio-cleanup-warning#fe31e5252a078624b7c42105a823050f51a2d76f" } +version = "2.14.2.dev96+da965db5" +source = { git = "https://github.com/jlowin/fastmcp?rev=main#da965db5cb460aef9f0341556a2bd4ad5f61846c" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, From 3dd7ce0f3874ed01196b2a5ca7dda1b57b337c01 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:39:07 -0500 Subject: [PATCH 03/10] Fix event loop warning with keep_alive=False for stdio MCP servers --- pyproject.toml | 4 +++- src/colin/providers/mcp.py | 4 ++++ uv.lock | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7eb1de3..8481b59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,9 @@ env = [ "COLIN_DEFAULT_LLM_MODEL=test", ] filterwarnings = [ - "ignore::pytest.PytestUnraisableExceptionWarning:.*Event loop is closed.*", + # With keep_alive=False on stdio MCP servers, the subprocess cleanup warning + # should no longer occur. Setting to error to verify the fix works. + "error::pytest.PytestUnraisableExceptionWarning", ] [tool.ty.terminal] diff --git a/src/colin/providers/mcp.py b/src/colin/providers/mcp.py index a692e3b..083cc69 100644 --- a/src/colin/providers/mcp.py +++ b/src/colin/providers/mcp.py @@ -112,6 +112,10 @@ def from_config(cls, name: str | None, config: dict[str, Any]) -> Self: """ if not name: raise ValueError("MCP provider requires an instance name") + # Set keep_alive=False for stdio servers to ensure proper subprocess cleanup + # and avoid "Event loop is closed" warnings during shutdown. + if "command" in config and "keep_alive" not in config: + config = {**config, "keep_alive": False} server = MCPServerAdapter.validate_python(config) return cls(name, server) diff --git a/uv.lock b/uv.lock index 35847d3..7ba7eb8 100644 --- a/uv.lock +++ b/uv.lock @@ -862,8 +862,8 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.14.2.dev96+da965db5" -source = { git = "https://github.com/jlowin/fastmcp?rev=main#da965db5cb460aef9f0341556a2bd4ad5f61846c" } +version = "2.14.2.dev97+a117316b" +source = { git = "https://github.com/jlowin/fastmcp?rev=main#a117316ba1df3f422be4c846ca70311fa2d1f1cf" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, From 91500cc64a7754079166519a209f3e6eb616833d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:58:09 -0500 Subject: [PATCH 04/10] Fix examples --- examples/everything/colin.toml | 7 +-- examples/everything/models/analysis.md | 10 +++- examples/everything/models/executive_brief.md | 8 +-- examples/everything/models/goal_status.md | 4 +- examples/everything/models/metrics_api.md | 3 +- examples/everything/models/recommendations.md | 4 +- examples/hello_world/models/summary.md | 6 +- examples/hello_world/models/welcome.md | 4 +- examples/mcp/.colin/compiled/greeting.md | 9 +++ examples/mcp/.colin/manifest.json | 55 +++++++++++++++++++ examples/mcp/colin.toml | 18 ++++++ examples/mcp/mcp_server.py | 21 +++++++ examples/mcp/models/greeting.md | 13 +++++ examples/mcp/output/greeting.md | 9 +++ src/colin/compiler/engine.py | 11 ++-- src/colin/providers/base.py | 4 +- 16 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 examples/mcp/.colin/compiled/greeting.md create mode 100644 examples/mcp/.colin/manifest.json create mode 100644 examples/mcp/colin.toml create mode 100644 examples/mcp/mcp_server.py create mode 100644 examples/mcp/models/greeting.md create mode 100644 examples/mcp/output/greeting.md diff --git a/examples/everything/colin.toml b/examples/everything/colin.toml index d5c4e79..f636f29 100644 --- a/examples/everything/colin.toml +++ b/examples/everything/colin.toml @@ -1,10 +1,7 @@ [project] name = "everything" -version = "0.1.0" - -[paths] -models = "models" -target = "target" +model-path = "models" +output-path = "output" [[providers.llm]] model = "anthropic:claude-haiku-4-5" diff --git a/examples/everything/models/analysis.md b/examples/everything/models/analysis.md index 09ae88a..3b17aaa 100644 --- a/examples/everything/models/analysis.md +++ b/examples/everything/models/analysis.md @@ -7,13 +7,17 @@ description: Extracted insights from product data {{ colin.mcp.demo.resource('demo://greeting/Analyst') }} ## Key Pain Points -{{ ref('data') | llm_extract('List the top 3 user pain points mentioned in the feedback, as bullet points') }} + +{{ ref('data.md') | llm_extract('List the top 3 user pain points mentioned in the feedback, as bullet points') }} ## Performance Summary -{{ ref('data') | llm_extract('Summarize the technical performance in one sentence') }} + +{{ ref('data.md') | llm_extract('Summarize the technical performance in one sentence') }} ## Strengths -{{ ref('data') | llm_extract('What are users happy about? List as bullet points') }} + +{{ ref('data.md') | llm_extract('What are users happy about? List as bullet points') }} ## Analysis Guidance + {{ colin.mcp.demo.prompt('summarize', style='detailed') }} diff --git a/examples/everything/models/executive_brief.md b/examples/everything/models/executive_brief.md index 4f64a4d..32eff6f 100644 --- a/examples/everything/models/executive_brief.md +++ b/examples/everything/models/executive_brief.md @@ -5,13 +5,13 @@ description: High-level summary for leadership # Executive Brief: Q1 2025 Planning ## TL;DR -{{ ref('recommendations') | llm_extract('Summarize the top 3 priorities in one sentence each') }} +{{ ref('recommendations.md') | llm_extract('Summarize the top 3 priorities in one sentence each') }} ## Current State -{{ ref('goal_status') | llm_extract('Give a one-paragraph executive summary of goal progress') }} +{{ ref('goal_status.md') | llm_extract('Give a one-paragraph executive summary of goal progress') }} ## Detailed Recommendations -{{ ref('recommendations').content }} +{{ ref('recommendations.md').content }} --- @@ -19,5 +19,5 @@ description: High-level summary for leadership
Click to expand source data -{{ ref('data').content }} +{{ ref('data.md').content }}
diff --git a/examples/everything/models/goal_status.md b/examples/everything/models/goal_status.md index cef8818..6869eb3 100644 --- a/examples/everything/models/goal_status.md +++ b/examples/everything/models/goal_status.md @@ -7,10 +7,10 @@ description: Assessment of progress toward goals Based on current metrics and stated objectives: **Current Data:** -{{ ref('data').content }} +{{ ref('data.md').content }} **Goals:** -{{ ref('goals').content }} +{{ ref('goals.md').content }} {% llm %} Compare the current metrics from the data above against the stated goals. diff --git a/examples/everything/models/metrics_api.md b/examples/everything/models/metrics_api.md index 9d890d5..435f3d2 100644 --- a/examples/everything/models/metrics_api.md +++ b/examples/everything/models/metrics_api.md @@ -2,7 +2,8 @@ name: Metrics API description: Structured JSON output for API consumption colin: - output: json + output: + format: json --- ## product diff --git a/examples/everything/models/recommendations.md b/examples/everything/models/recommendations.md index fa85efe..7b98f2f 100644 --- a/examples/everything/models/recommendations.md +++ b/examples/everything/models/recommendations.md @@ -5,10 +5,10 @@ description: Prioritized action items based on analysis # Strategic Recommendations ## Analysis Summary -{{ ref('analysis').content }} +{{ ref('analysis.md').content }} ## Goal Status -{{ ref('goal_status') | llm_extract('Which goals are at risk or behind?') }} +{{ ref('goal_status.md') | llm_extract('Which goals are at risk or behind?') }} ## Recommended Actions diff --git a/examples/hello_world/models/summary.md b/examples/hello_world/models/summary.md index 6dec424..749119a 100644 --- a/examples/hello_world/models/summary.md +++ b/examples/hello_world/models/summary.md @@ -7,19 +7,19 @@ description: Demonstrates LLM blocks and extract filter Here's some content to work with: -{{ ref('greeting').content }} +{{ ref('greeting.md').content }} ## Extracted Info -{{ ref('greeting') | llm_extract('the main message in one sentence') }} +{{ ref('greeting.md') | llm_extract('the main message in one sentence') }} ## LLM-Generated Content {% llm %} Given this greeting: -{{ ref('greeting').content }} +{{ ref('greeting.md').content }} Write a haiku about being welcomed. {% endllm %} diff --git a/examples/hello_world/models/welcome.md b/examples/hello_world/models/welcome.md index 19249aa..beedcca 100644 --- a/examples/hello_world/models/welcome.md +++ b/examples/hello_world/models/welcome.md @@ -5,7 +5,7 @@ description: Demonstrates ref() and LLM blocks # Welcome -{{ ref('greeting').content }} +{{ ref('greeting.md').content }} --- @@ -17,7 +17,7 @@ Colin automatically compiles documents in the right order based on their depende {% llm %} Translate this greeting message for French users of the Colin library: -{{ ref('greeting').content }} +{{ ref('greeting.md').content }} The translation should feel welcoming and appropriate for a technical audience. {% endllm %} diff --git a/examples/mcp/.colin/compiled/greeting.md b/examples/mcp/.colin/compiled/greeting.md new file mode 100644 index 0000000..bf9578d --- /dev/null +++ b/examples/mcp/.colin/compiled/greeting.md @@ -0,0 +1,9 @@ +# MCP Demo + +## Resource Example + +Hello, World! Welcome to the demo. + +## Prompt Example + +Summarize the following in a detailed style. \ No newline at end of file diff --git a/examples/mcp/.colin/manifest.json b/examples/mcp/.colin/manifest.json new file mode 100644 index 0000000..1cc2515 --- /dev/null +++ b/examples/mcp/.colin/manifest.json @@ -0,0 +1,55 @@ +{ + "version": "1", + "project_name": "mcp", + "config_hash": "f6a9b770bee3be5d", + "compiled_at": "2026-01-07T21:57:28.998543Z", + "documents": { + "project://greeting.md": { + "uri": "project://greeting.md", + "source_path": null, + "source_hash": "44d23c70d58c7e85", + "output_hash": "326780bff734af74", + "output_path": "greeting.md", + "is_published": true, + "compiled_at": "2026-01-07T21:53:49.341310Z", + "refs": [ + { + "provider": "mcp", + "connection": "demo", + "method": "config", + "args": {} + }, + { + "provider": "mcp", + "connection": "demo", + "method": "resource", + "args": { + "uri": "demo://greeting/World" + } + }, + { + "provider": "mcp", + "connection": "demo", + "method": "prompt", + "args": { + "name": "summarize", + "arguments": { + "style": "detailed" + } + } + } + ], + "ref_versions": { + "{\"args\": {}, \"connection\": \"demo\", \"method\": \"config\", \"provider\": \"mcp\"}": "e0569178996485f6", + "{\"args\": {\"uri\": \"demo://greeting/World\"}, \"connection\": \"demo\", \"method\": \"resource\", \"provider\": \"mcp\"}": "6236c65af4fdeed5", + "{\"args\": {\"arguments\": {\"style\": \"detailed\"}, \"name\": \"summarize\"}, \"connection\": \"demo\", \"method\": \"prompt\", \"provider\": \"mcp\"}": "e31eb0daa2d308b8" + }, + "llm_calls": {}, + "total_cost_usd": 0.0, + "artifacts": [], + "sections": {}, + "config_hash": "f6a9b770bee3be5d" + } + }, + "cache": {} +} \ No newline at end of file diff --git a/examples/mcp/colin.toml b/examples/mcp/colin.toml new file mode 100644 index 0000000..1d17256 --- /dev/null +++ b/examples/mcp/colin.toml @@ -0,0 +1,18 @@ +[project] +name = "mcp" +model-path = "models" +output-path = "output" + +[[providers.mcp]] +name = "demo" +command = "uvx" +args = [ + "--with", + "fastmcp", + "fastmcp", + "run", + "--no-banner", + "--log-level", + "ERROR", + "examples/mcp/mcp_server.py", +] diff --git a/examples/mcp/mcp_server.py b/examples/mcp/mcp_server.py new file mode 100644 index 0000000..0a0d807 --- /dev/null +++ b/examples/mcp/mcp_server.py @@ -0,0 +1,21 @@ +"""Simple MCP server for the mcp example.""" + +from fastmcp import FastMCP + +mcp = FastMCP("demo") + + +@mcp.resource(uri="demo://greeting/{name}") +def greeting(name: str) -> str: + """Get a personalized greeting.""" + return f"Hello, {name}! Welcome to the demo." + + +@mcp.prompt() +def summarize(style: str = "brief") -> str: + """Summarization guidance.""" + return f"Summarize the following in a {style} style." + + +if __name__ == "__main__": + mcp.run() diff --git a/examples/mcp/models/greeting.md b/examples/mcp/models/greeting.md new file mode 100644 index 0000000..43a5a88 --- /dev/null +++ b/examples/mcp/models/greeting.md @@ -0,0 +1,13 @@ +--- +name: MCP Greeting +description: Demonstrates MCP resource and prompt usage +--- +# MCP Demo + +## Resource Example + +{{ colin.mcp.demo.resource('demo://greeting/World') }} + +## Prompt Example + +{{ colin.mcp.demo.prompt('summarize', style='detailed') }} diff --git a/examples/mcp/output/greeting.md b/examples/mcp/output/greeting.md new file mode 100644 index 0000000..bf9578d --- /dev/null +++ b/examples/mcp/output/greeting.md @@ -0,0 +1,9 @@ +# MCP Demo + +## Resource Example + +Hello, World! Welcome to the demo. + +## Prompt Example + +Summarize the following in a detailed style. \ No newline at end of file diff --git a/src/colin/compiler/engine.py b/src/colin/compiler/engine.py index 2d7ec02..4e59e1c 100644 --- a/src/colin/compiler/engine.py +++ b/src/colin/compiler/engine.py @@ -485,17 +485,20 @@ def _load_document(self, path: Path) -> ColinDocument: content = path.read_text(encoding="utf-8") post = fm_parser.loads(content) + # Build relative path for URIs and error messages + relative = path.relative_to(self.config.model_path) + # Extract colin config raw_colin = post.metadata.pop("colin", {}) colin_data = cast(dict[str, Any], raw_colin) if isinstance(raw_colin, dict) else {} - colin_config = ColinConfig.model_validate(colin_data) + try: + colin_config = ColinConfig.model_validate(colin_data) + except Exception as e: + raise ValueError(f"Invalid frontmatter in {relative}:\n{e}") from e # Rest is document metadata metadata = cast(dict[str, Any], post.metadata) frontmatter = Frontmatter(colin=colin_config, metadata=metadata) - - # Build URI from path - relative = path.relative_to(self.config.model_path) uri = f"project://{relative}" # Hash the FULL content (including frontmatter) for change detection diff --git a/src/colin/providers/base.py b/src/colin/providers/base.py index e2c4096..5f9b2f2 100644 --- a/src/colin/providers/base.py +++ b/src/colin/providers/base.py @@ -1,5 +1,7 @@ """Provider base class.""" +import hashlib +import json from collections.abc import AsyncIterator, Awaitable, Callable from contextlib import asynccontextmanager from typing import TYPE_CHECKING, Any, ClassVar @@ -94,8 +96,6 @@ def from_config(cls, name: str | None, config: dict[str, Any]) -> Self: Returns: Configured provider instance. """ - import hashlib - import json instance = cls(**config) instance._connection = name or "" From 768d932a933d0dc28f6e6b01bf0b4547859d600d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:01:18 -0500 Subject: [PATCH 05/10] Temporarily disable S3 tests to isolate event loop warning --- tests/providers/{test_s3.py => test_s3.py.bak} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/providers/{test_s3.py => test_s3.py.bak} (100%) diff --git a/tests/providers/test_s3.py b/tests/providers/test_s3.py.bak similarity index 100% rename from tests/providers/test_s3.py rename to tests/providers/test_s3.py.bak From 1fd84d2909e7a140b653f5bfbf04b6caa890e0f0 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:13:46 -0500 Subject: [PATCH 06/10] Restore S3 tests and warning filter --- pyproject.toml | 4 +--- tests/providers/{test_s3.py.bak => test_s3.py} | 0 2 files changed, 1 insertion(+), 3 deletions(-) rename tests/providers/{test_s3.py.bak => test_s3.py} (100%) diff --git a/pyproject.toml b/pyproject.toml index 8481b59..7eb1de3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,7 @@ env = [ "COLIN_DEFAULT_LLM_MODEL=test", ] filterwarnings = [ - # With keep_alive=False on stdio MCP servers, the subprocess cleanup warning - # should no longer occur. Setting to error to verify the fix works. - "error::pytest.PytestUnraisableExceptionWarning", + "ignore::pytest.PytestUnraisableExceptionWarning:.*Event loop is closed.*", ] [tool.ty.terminal] diff --git a/tests/providers/test_s3.py.bak b/tests/providers/test_s3.py similarity index 100% rename from tests/providers/test_s3.py.bak rename to tests/providers/test_s3.py From bd641a2a3968191684f92ea7f5d7f1918a0d3de3 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:00:51 -0500 Subject: [PATCH 07/10] Fix examples --- src/colin/providers/mcp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/colin/providers/mcp.py b/src/colin/providers/mcp.py index 083cc69..129f081 100644 --- a/src/colin/providers/mcp.py +++ b/src/colin/providers/mcp.py @@ -2,6 +2,7 @@ from __future__ import annotations +import gc from collections.abc import AsyncIterator, Awaitable, Callable from contextlib import asynccontextmanager, nullcontext from typing import Any, ClassVar @@ -129,6 +130,10 @@ async def lifespan(self) -> AsyncIterator[None]: self._client = client yield self._client = None + # Force GC to clean up subprocess transports while event loop is still running. + # Without this, transport cleanup may be deferred until after the loop closes, + # causing "Event loop is closed" warnings on Linux. + gc.collect() def _require_client(self) -> Client: """Get client, raising if not initialized.""" From 6cb2e2b9a796bdb659c7c249a88925148d24e88b Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:09:45 -0500 Subject: [PATCH 08/10] Fix filterwarnings syntax --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7eb1de3..e125718 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ env = [ "COLIN_DEFAULT_LLM_MODEL=test", ] filterwarnings = [ - "ignore::pytest.PytestUnraisableExceptionWarning:.*Event loop is closed.*", + "ignore:.*Event loop is closed.*:pytest.PytestUnraisableExceptionWarning", ] [tool.ty.terminal] From f25fb7dcd87bc26237bd9da27f299e2466fbea3d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:10:13 -0500 Subject: [PATCH 09/10] Remove ineffective gc.collect() --- src/colin/providers/mcp.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/colin/providers/mcp.py b/src/colin/providers/mcp.py index 129f081..083cc69 100644 --- a/src/colin/providers/mcp.py +++ b/src/colin/providers/mcp.py @@ -2,7 +2,6 @@ from __future__ import annotations -import gc from collections.abc import AsyncIterator, Awaitable, Callable from contextlib import asynccontextmanager, nullcontext from typing import Any, ClassVar @@ -130,10 +129,6 @@ async def lifespan(self) -> AsyncIterator[None]: self._client = client yield self._client = None - # Force GC to clean up subprocess transports while event loop is still running. - # Without this, transport cleanup may be deferred until after the loop closes, - # causing "Event loop is closed" warnings on Linux. - gc.collect() def _require_client(self) -> Client: """Get client, raising if not initialized.""" From 3c8a0fa6d7cfaccaf3da1fd330994ccdec233ea9 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:12:33 -0500 Subject: [PATCH 10/10] Fix filterwarnings to match actual warning message --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e125718..3694bf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ env = [ "COLIN_DEFAULT_LLM_MODEL=test", ] filterwarnings = [ - "ignore:.*Event loop is closed.*:pytest.PytestUnraisableExceptionWarning", + "ignore:.*BaseSubprocessTransport.*:pytest.PytestUnraisableExceptionWarning", ] [tool.ty.terminal]