scaffold-ca-python 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. scaffold_ca_python/__init__.py +1 -0
  2. scaffold_ca_python/cli.py +39 -0
  3. scaffold_ca_python/commands/__init__.py +0 -0
  4. scaffold_ca_python/commands/delete_module.py +216 -0
  5. scaffold_ca_python/commands/generate_driven_adapter.py +182 -0
  6. scaffold_ca_python/commands/generate_entry_point.py +304 -0
  7. scaffold_ca_python/commands/generate_helper.py +135 -0
  8. scaffold_ca_python/commands/generate_model.py +134 -0
  9. scaffold_ca_python/commands/generate_pipeline.py +158 -0
  10. scaffold_ca_python/commands/generate_project.py +189 -0
  11. scaffold_ca_python/commands/generate_use_case.py +136 -0
  12. scaffold_ca_python/commands/update_project.py +84 -0
  13. scaffold_ca_python/commands/validate_structure.py +90 -0
  14. scaffold_ca_python/core/__init__.py +0 -0
  15. scaffold_ca_python/core/file_writer.py +128 -0
  16. scaffold_ca_python/core/module_builder.py +127 -0
  17. scaffold_ca_python/core/name_utils.py +59 -0
  18. scaffold_ca_python/core/project_detector.py +93 -0
  19. scaffold_ca_python/core/pyproject_writer.py +169 -0
  20. scaffold_ca_python/core/structure_validator.py +142 -0
  21. scaffold_ca_python/core/template_renderer.py +100 -0
  22. scaffold_ca_python/factory/__init__.py +16 -0
  23. scaffold_ca_python/factory/driven_adapters/__init__.py +0 -0
  24. scaffold_ca_python/factory/driven_adapters/da_generic.py +65 -0
  25. scaffold_ca_python/factory/driven_adapters/da_rest_consumer.py +64 -0
  26. scaffold_ca_python/factory/driven_adapters/da_secrets.py +64 -0
  27. scaffold_ca_python/factory/entry_points/__init__.py +0 -0
  28. scaffold_ca_python/factory/entry_points/ep_agent.py +91 -0
  29. scaffold_ca_python/factory/entry_points/ep_generic.py +75 -0
  30. scaffold_ca_python/factory/entry_points/ep_mcp.py +138 -0
  31. scaffold_ca_python/factory/entry_points/ep_restapi.py +133 -0
  32. scaffold_ca_python/factory/simple/__init__.py +0 -0
  33. scaffold_ca_python/factory/simple/delete_module_factory.py +85 -0
  34. scaffold_ca_python/factory/simple/helper_factory.py +67 -0
  35. scaffold_ca_python/factory/simple/model_factory.py +57 -0
  36. scaffold_ca_python/factory/simple/use_case_factory.py +59 -0
  37. scaffold_ca_python/models/__init__.py +0 -0
  38. scaffold_ca_python/models/context.py +60 -0
  39. scaffold_ca_python/models/file_operation.py +47 -0
  40. scaffold_ca_python/models/layer.py +41 -0
  41. scaffold_ca_python/models/violation.py +26 -0
  42. scaffold_ca_python/templates/__init__.py +0 -0
  43. scaffold_ca_python/templates/driven_adapter/generic/__init__.py.jinja2 +1 -0
  44. scaffold_ca_python/templates/driven_adapter/generic/adapter.py.jinja2 +18 -0
  45. scaffold_ca_python/templates/driven_adapter/generic/test_adapter.py.jinja2 +22 -0
  46. scaffold_ca_python/templates/driven_adapter/rest_consumer/__init__.py.jinja2 +1 -0
  47. scaffold_ca_python/templates/driven_adapter/rest_consumer/rest_consumer.py.jinja2 +27 -0
  48. scaffold_ca_python/templates/driven_adapter/rest_consumer/test_rest_consumer.py.jinja2 +24 -0
  49. scaffold_ca_python/templates/driven_adapter/secrets/__init__.py.jinja2 +1 -0
  50. scaffold_ca_python/templates/driven_adapter/secrets/secrets_adapter.py.jinja2 +37 -0
  51. scaffold_ca_python/templates/driven_adapter/secrets/test_secrets_adapter.py.jinja2 +26 -0
  52. scaffold_ca_python/templates/entry_point/agent/__init__.py.jinja2 +1 -0
  53. scaffold_ca_python/templates/entry_point/agent/agent.py.jinja2 +49 -0
  54. scaffold_ca_python/templates/entry_point/agent/card.py.jinja2 +15 -0
  55. scaffold_ca_python/templates/entry_point/agent/entrypoint_main.py.jinja2 +13 -0
  56. scaffold_ca_python/templates/entry_point/agent/test_agent.py.jinja2 +20 -0
  57. scaffold_ca_python/templates/entry_point/generic/__init__.py.jinja2 +1 -0
  58. scaffold_ca_python/templates/entry_point/generic/entrypoint_main.py.jinja2 +13 -0
  59. scaffold_ca_python/templates/entry_point/generic/handler.py.jinja2 +13 -0
  60. scaffold_ca_python/templates/entry_point/generic/test_handler.py.jinja2 +35 -0
  61. scaffold_ca_python/templates/entry_point/mcp/__init__.py.jinja2 +1 -0
  62. scaffold_ca_python/templates/entry_point/mcp/app.py.jinja2 +51 -0
  63. scaffold_ca_python/templates/entry_point/mcp/prompts.py.jinja2 +22 -0
  64. scaffold_ca_python/templates/entry_point/mcp/resources.py.jinja2 +22 -0
  65. scaffold_ca_python/templates/entry_point/mcp/server.py.jinja2 +27 -0
  66. scaffold_ca_python/templates/entry_point/mcp/test_app.py.jinja2 +32 -0
  67. scaffold_ca_python/templates/entry_point/mcp/test_prompts.py.jinja2 +40 -0
  68. scaffold_ca_python/templates/entry_point/mcp/test_resources.py.jinja2 +47 -0
  69. scaffold_ca_python/templates/entry_point/mcp/test_tools.py.jinja2 +40 -0
  70. scaffold_ca_python/templates/entry_point/mcp/tools.py.jinja2 +22 -0
  71. scaffold_ca_python/templates/entry_point/restapi/__init__.py.jinja2 +1 -0
  72. scaffold_ca_python/templates/entry_point/restapi/app.py.jinja2 +78 -0
  73. scaffold_ca_python/templates/entry_point/restapi/exception_handler.py.jinja2 +35 -0
  74. scaffold_ca_python/templates/entry_point/restapi/health.py.jinja2 +13 -0
  75. scaffold_ca_python/templates/entry_point/restapi/rest_controller.py.jinja2 +26 -0
  76. scaffold_ca_python/templates/entry_point/restapi/server.py.jinja2 +5 -0
  77. scaffold_ca_python/templates/entry_point/restapi/test_app.py.jinja2 +22 -0
  78. scaffold_ca_python/templates/entry_point/restapi/test_exception_handler.py.jinja2 +44 -0
  79. scaffold_ca_python/templates/entry_point/restapi/test_rest_controller.py.jinja2 +35 -0
  80. scaffold_ca_python/templates/entry_point/restapi/test_server.py.jinja2 +15 -0
  81. scaffold_ca_python/templates/helper/__init__.py.jinja2 +1 -0
  82. scaffold_ca_python/templates/helper/helper.py.jinja2 +7 -0
  83. scaffold_ca_python/templates/helper/test_helper.py.jinja2 +8 -0
  84. scaffold_ca_python/templates/model/model.py.jinja2 +9 -0
  85. scaffold_ca_python/templates/model/test_model.py.jinja2 +8 -0
  86. scaffold_ca_python/templates/pipeline/azure/azure_pipelines.yml.jinja2 +28 -0
  87. scaffold_ca_python/templates/pipeline/github/ci.yml.jinja2 +34 -0
  88. scaffold_ca_python/templates/project/README.jinja2 +30 -0
  89. scaffold_ca_python/templates/project/application/config/__init__.py.jinja2 +1 -0
  90. scaffold_ca_python/templates/project/application/config/config.py.jinja2 +12 -0
  91. scaffold_ca_python/templates/project/application/config/container.py.jinja2 +17 -0
  92. scaffold_ca_python/templates/project/application/config/driven_adapters_container.py.jinja2 +14 -0
  93. scaffold_ca_python/templates/project/application/config/resource_container.py.jinja2 +17 -0
  94. scaffold_ca_python/templates/project/application/config/usecases_container.py.jinja2 +16 -0
  95. scaffold_ca_python/templates/project/dockerfile.jinja2 +22 -0
  96. scaffold_ca_python/templates/project/dockerignore.jinja2 +19 -0
  97. scaffold_ca_python/templates/project/gitignore.jinja2 +64 -0
  98. scaffold_ca_python/templates/project/layer_init.jinja2 +1 -0
  99. scaffold_ca_python/templates/project/main.py.jinja2 +10 -0
  100. scaffold_ca_python/templates/project/mypy_ini.jinja2 +5 -0
  101. scaffold_ca_python/templates/project/pyproject_toml.jinja2 +66 -0
  102. scaffold_ca_python/templates/project/python_version.jinja2 +1 -0
  103. scaffold_ca_python/templates/use_case/test_use_case.py.jinja2 +12 -0
  104. scaffold_ca_python/templates/use_case/use_case.py.jinja2 +9 -0
  105. scaffold_ca_python-0.1.1.dist-info/METADATA +285 -0
  106. scaffold_ca_python-0.1.1.dist-info/RECORD +109 -0
  107. scaffold_ca_python-0.1.1.dist-info/WHEEL +4 -0
  108. scaffold_ca_python-0.1.1.dist-info/entry_points.txt +3 -0
  109. scaffold_ca_python-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,41 @@
1
+ """Layer enum and FORBIDDEN_IMPORTS dependency mapping (T013)."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class Layer(StrEnum):
7
+ DOMAIN_MODEL = "domain/model"
8
+ DOMAIN_USECASE = "domain/usecase"
9
+ ENTRY_POINTS = "infrastructure/entry-points"
10
+ DRIVEN_ADAPTERS = "infrastructure/driven-adapters"
11
+ HELPERS = "infrastructure/helpers"
12
+ APPLICATION = "application"
13
+
14
+
15
+ # Maps each inner layer to the set of outer layers it must NOT import from.
16
+ # Layers not present as keys (APPLICATION) may import from any layer.
17
+ FORBIDDEN_IMPORTS: dict[Layer, list[Layer]] = {
18
+ Layer.DOMAIN_MODEL: [
19
+ Layer.DOMAIN_USECASE,
20
+ Layer.ENTRY_POINTS,
21
+ Layer.DRIVEN_ADAPTERS,
22
+ Layer.HELPERS,
23
+ Layer.APPLICATION,
24
+ ],
25
+ Layer.DOMAIN_USECASE: [
26
+ Layer.ENTRY_POINTS,
27
+ Layer.DRIVEN_ADAPTERS,
28
+ Layer.HELPERS,
29
+ Layer.APPLICATION,
30
+ ],
31
+ Layer.ENTRY_POINTS: [
32
+ Layer.APPLICATION,
33
+ ],
34
+ Layer.DRIVEN_ADAPTERS: [
35
+ Layer.APPLICATION,
36
+ ],
37
+ Layer.HELPERS: [
38
+ Layer.APPLICATION,
39
+ ],
40
+ Layer.APPLICATION: [],
41
+ }
@@ -0,0 +1,26 @@
1
+ """Violation and ValidationReport Pydantic v2 models (T016)."""
2
+
3
+ from pathlib import Path
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from scaffold_ca_python.models.layer import Layer
8
+
9
+
10
+ class Violation(BaseModel):
11
+ source_file: Path
12
+ line_number: int
13
+ import_statement: str
14
+ source_layer: Layer
15
+ target_layer: Layer
16
+ resolution_hint: str
17
+
18
+
19
+ class ValidationReport(BaseModel):
20
+ project_root: Path
21
+ files_scanned: int
22
+ violations: list[Violation]
23
+
24
+ @property
25
+ def passed(self) -> bool:
26
+ return len(self.violations) == 0
File without changes
@@ -0,0 +1 @@
1
+ """{{ class_name }} adapter package."""
@@ -0,0 +1,18 @@
1
+ """{{ class_name }} async driven adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class {{ class_name }}Adapter:
7
+ """Async driven adapter for {{ class_name }}.
8
+
9
+ Implement the methods below to integrate with your external resource.
10
+ """
11
+
12
+ async def execute(self) -> None:
13
+ """Perform the adapter's primary operation.
14
+
15
+ Raises:
16
+ NotImplementedError: Replace with the concrete implementation.
17
+ """
18
+ raise NotImplementedError
@@ -0,0 +1,22 @@
1
+ """Tests for {{ class_name }}Adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from {{ project.python_package }}.infrastructure.driven_adapters.{{ module_name }}.{{ module_name }}_adapter import (
8
+ {{ class_name }}Adapter,
9
+ )
10
+
11
+
12
+ @pytest.fixture()
13
+ def adapter() -> {{ class_name }}Adapter:
14
+ return {{ class_name }}Adapter()
15
+
16
+
17
+ @pytest.mark.asyncio
18
+ async def test_{{ module_name }}_adapter_execute_raises_not_implemented(
19
+ adapter: {{ class_name }}Adapter,
20
+ ) -> None:
21
+ with pytest.raises(NotImplementedError):
22
+ await adapter.execute()
@@ -0,0 +1 @@
1
+ """REST consumer driven adapter package."""
@@ -0,0 +1,27 @@
1
+ """Async HTTP client adapter using httpx."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+
8
+ class RestConsumerAdapter:
9
+ """Async REST consumer adapter.
10
+
11
+ Wraps :class:`httpx.AsyncClient` to provide a thin, testable HTTP boundary.
12
+ Replace the placeholder methods with your actual API calls.
13
+ """
14
+
15
+ def __init__(self, base_url: str = "", timeout: float = 10.0) -> None:
16
+ self._base_url = base_url
17
+ self._timeout = timeout
18
+
19
+ async def get(self, path: str, **kwargs: object) -> httpx.Response:
20
+ """Perform an async GET request."""
21
+ async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as client:
22
+ return await client.get(path, **kwargs)
23
+
24
+ async def post(self, path: str, **kwargs: object) -> httpx.Response:
25
+ """Perform an async POST request."""
26
+ async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as client:
27
+ return await client.post(path, **kwargs)
@@ -0,0 +1,24 @@
1
+ """Tests for RestConsumerAdapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ import respx
7
+ import httpx
8
+
9
+ from {{ project.python_package }}.infrastructure.driven_adapters.rest_consumer.rest_consumer import (
10
+ RestConsumerAdapter,
11
+ )
12
+
13
+
14
+ @pytest.fixture()
15
+ def adapter() -> RestConsumerAdapter:
16
+ return RestConsumerAdapter(base_url="https://example.com")
17
+
18
+
19
+ @respx.mock
20
+ @pytest.mark.asyncio
21
+ async def test_get_returns_response(adapter: RestConsumerAdapter) -> None:
22
+ respx.get("https://example.com/ping").mock(return_value=httpx.Response(200))
23
+ response = await adapter.get("/ping")
24
+ assert response.status_code == 200
@@ -0,0 +1 @@
1
+ """Secrets adapter package."""
@@ -0,0 +1,37 @@
1
+ """Async secrets-store adapter (provider-agnostic interface)."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class SecretsAdapter:
7
+ """Fetches secrets from an external secrets store.
8
+
9
+ Replace the body of :meth:`get_secret` with the call to your actual
10
+ provider (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, etc.).
11
+ """
12
+
13
+ async def get_secret(self, secret_name: str) -> str:
14
+ """Return the value of a named secret.
15
+
16
+ Args:
17
+ secret_name: Provider-specific identifier of the secret.
18
+
19
+ Returns:
20
+ The secret value as a plain string.
21
+
22
+ Raises:
23
+ NotImplementedError: Until the concrete provider is wired in.
24
+ """
25
+ raise NotImplementedError(f"Provider not configured for secret '{secret_name}'.")
26
+
27
+ async def set_secret(self, secret_name: str, value: str) -> None:
28
+ """Write or update a named secret.
29
+
30
+ Args:
31
+ secret_name: Provider-specific identifier of the secret.
32
+ value: The new secret value.
33
+
34
+ Raises:
35
+ NotImplementedError: Until the concrete provider is wired in.
36
+ """
37
+ raise NotImplementedError(f"Provider not configured for secret '{secret_name}'.")
@@ -0,0 +1,26 @@
1
+ """Tests for SecretsAdapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from {{ project.python_package }}.infrastructure.driven_adapters.secrets.secrets_adapter import (
8
+ SecretsAdapter,
9
+ )
10
+
11
+
12
+ @pytest.fixture()
13
+ def adapter() -> SecretsAdapter:
14
+ return SecretsAdapter()
15
+
16
+
17
+ @pytest.mark.asyncio
18
+ async def test_get_secret_raises_not_implemented(adapter: SecretsAdapter) -> None:
19
+ with pytest.raises(NotImplementedError):
20
+ await adapter.get_secret("my-secret")
21
+
22
+
23
+ @pytest.mark.asyncio
24
+ async def test_set_secret_raises_not_implemented(adapter: SecretsAdapter) -> None:
25
+ with pytest.raises(NotImplementedError):
26
+ await adapter.set_secret("my-secret", "value")
@@ -0,0 +1 @@
1
+ """Agent entry-point package."""
@@ -0,0 +1,49 @@
1
+ """A2A async agent handler stub."""
2
+
3
+ from __future__ import annotations
4
+
5
+ {% if enable_kafka %}
6
+ # Kafka consumer stub — replace with your actual broker configuration.
7
+ # from aiokafka import AIOKafkaConsumer
8
+ {% endif %}
9
+ {% if enable_mcp_client %}
10
+ # MCP tool-call client stub — replace with your actual MCP client.
11
+ # from mcp import ClientSession
12
+ {% endif %}
13
+
14
+
15
+ class {{ project.name }}Agent:
16
+ """Agent-to-Agent (A2A) protocol handler for {{ project.name }}.
17
+
18
+ Implement :meth:`run` to handle incoming agent task requests.
19
+ """
20
+
21
+ {% if enable_kafka %}
22
+ async def consume(self, topic: str) -> None:
23
+ """Consume messages from a Kafka topic."""
24
+ # consumer = AIOKafkaConsumer(topic, bootstrap_servers="localhost:9092")
25
+ # await consumer.start()
26
+ raise NotImplementedError("Wire in your Kafka consumer here.")
27
+
28
+ {% endif %}
29
+ {% if enable_mcp_client %}
30
+ async def call_tool(self, tool_name: str, arguments: dict[str, object]) -> object:
31
+ """Invoke a tool via the MCP client."""
32
+ # async with ClientSession(...) as session:
33
+ # return await session.call_tool(tool_name, arguments)
34
+ raise NotImplementedError("Wire in your MCP client here.")
35
+
36
+ {% endif %}
37
+ async def run(self, task: dict[str, object]) -> dict[str, object]:
38
+ """Handle an incoming A2A task.
39
+
40
+ Args:
41
+ task: The incoming task payload.
42
+
43
+ Returns:
44
+ The agent response payload.
45
+
46
+ Raises:
47
+ NotImplementedError: Replace with your task handling logic.
48
+ """
49
+ raise NotImplementedError
@@ -0,0 +1,15 @@
1
+ """AgentCard definition for {{ project.name }}."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class AgentCard:
10
+ """Describes the capabilities and identity of this agent."""
11
+
12
+ name: str = "{{ project.name }}"
13
+ description: str = "An AI agent built with scaffold-ca-python."
14
+ version: str = "0.1.0"
15
+ capabilities: list[str] = field(default_factory=list)
@@ -0,0 +1,13 @@
1
+ """{{ name }} agent entrypoint."""
2
+ import asyncio
3
+
4
+ from {{ python_package }}.infrastructure.entry_points.agent.agent import run
5
+
6
+
7
+ def main() -> None:
8
+ """Start the agent runner."""
9
+ asyncio.run(run())
10
+
11
+
12
+ if __name__ == "__main__":
13
+ main()
@@ -0,0 +1,20 @@
1
+ """Tests for {{ project.name }}Agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from {{ project.python_package }}.infrastructure.entry_points.agent.agent import {{ project.name }}Agent
8
+ from {{ project.python_package }}.infrastructure.entry_points.agent.card import AgentCard
9
+
10
+
11
+ def test_agent_card_has_name() -> None:
12
+ card = AgentCard()
13
+ assert card.name == "{{ project.name }}"
14
+
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_agent_run_raises_not_implemented() -> None:
18
+ agent = {{ project.name }}Agent()
19
+ with pytest.raises(NotImplementedError):
20
+ await agent.run({})
@@ -0,0 +1 @@
1
+ """Generic entry-point package."""
@@ -0,0 +1,13 @@
1
+ """{{ name }} generic entrypoint."""
2
+ import asyncio
3
+
4
+ from {{ project.python_package }}.infrastructure.entry_points.generic.entry_point import run
5
+
6
+
7
+ def main() -> None:
8
+ """Run the generic handler."""
9
+ asyncio.run(run())
10
+
11
+
12
+ if __name__ == "__main__":
13
+ main()
@@ -0,0 +1,13 @@
1
+ """Generic async entry-point handler for {{ project.name }}."""
2
+
3
+ from __future__ import annotations
4
+
5
+ class EntryPoint:
6
+ """Generic async entry point.
7
+
8
+ Replace the body of :meth:`run` with your actual startup / handler logic.
9
+ """
10
+
11
+ async def run(self) -> None:
12
+ """Start the entry-point processing loop."""
13
+ print("Hello from the generic entry point! Replace this with your actual logic.")
@@ -0,0 +1,35 @@
1
+ """Tests for the generic entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from {{ project.python_package }}.infrastructure.entry_points.generic.entry_point import EntryPoint
8
+
9
+
10
+ class TestEntryPointRun:
11
+ """Test EntryPoint.run() behavior."""
12
+
13
+ @pytest.mark.asyncio
14
+ async def test_entry_point_run_is_async(self) -> None:
15
+ """Test that run method is async and callable."""
16
+ ep = EntryPoint()
17
+ result = await ep.run()
18
+ assert result is None
19
+
20
+ @pytest.mark.asyncio
21
+ async def test_entry_point_run_prints_message(self, capsys: pytest.CaptureFixture[str]) -> None:
22
+ """Test that run method prints the placeholder message."""
23
+ ep = EntryPoint()
24
+ await ep.run()
25
+ captured = capsys.readouterr()
26
+ assert "Hello from the generic entry point" in captured.out
27
+
28
+ @pytest.mark.asyncio
29
+ async def test_entry_point_run_does_not_raise_exception(self) -> None:
30
+ """Test that run method executes without raising exceptions."""
31
+ ep = EntryPoint()
32
+ try:
33
+ await ep.run()
34
+ except Exception as exc:
35
+ pytest.fail(f"EntryPoint.run() raised {type(exc).__name__}: {exc}")
@@ -0,0 +1 @@
1
+ """MCP server entry-point package."""
@@ -0,0 +1,51 @@
1
+ """MCP composition root — creates the FastMCP instance and Starlette ASGI app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import asynccontextmanager
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+ from starlette.applications import Starlette
9
+ from starlette.routing import Mount
10
+
11
+ from {{ project.python_package }}.application.config.container import Container
12
+ from {{ project.python_package }}.infrastructure.entry_points.mcp_server import tools
13
+ {% if with_resources %}from {{ project.python_package }}.infrastructure.entry_points.mcp_server import resources
14
+ {% endif %}{% if with_prompts %}from {{ project.python_package }}.infrastructure.entry_points.mcp_server import prompts
15
+ {% endif %}
16
+
17
+ mcp: FastMCP = FastMCP("{{ project.name }}", stateless_http=True)
18
+
19
+
20
+ @asynccontextmanager
21
+ async def lifespan(_app: Starlette) -> None:
22
+ """Wire the DI container, register all MCP primitives, then run the server."""
23
+ container = Container()
24
+ container.wire(
25
+ modules=[
26
+ tools,
27
+ {% if with_resources %}resources,{% endif %}
28
+ {% if with_prompts %}prompts,{% endif %}
29
+ ]
30
+ )
31
+
32
+ await tools.bind_tools(mcp)
33
+ {% if with_resources %}await resources.bind_resources(mcp)
34
+ {% endif %}{% if with_prompts %}await prompts.bind_prompts(mcp){% endif %}
35
+
36
+ # Uncomment the following lines if you have async resources to initialize on startup and shutdown
37
+ #await container.resource_container.init_resources()
38
+
39
+ async with mcp.session_manager.run():
40
+ yield
41
+
42
+ # Uncomment the following lines if you have async resources to initialize on startup and shutdown
43
+ #await container.resource_container.shutdown_resources()
44
+
45
+
46
+ def start_server() -> Starlette:
47
+ """Return a Starlette ASGI application wrapping the FastMCP instance."""
48
+ return Starlette(
49
+ routes=[Mount("/", app=mcp.streamable_http_app())],
50
+ lifespan=lifespan,
51
+ )
@@ -0,0 +1,22 @@
1
+ """MCP prompts binding — register FastMCP prompts via dependency injection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dependency_injector.wiring import Provide, inject
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from {{ project.python_package }}.application.config.container import Container
9
+
10
+
11
+ @inject
12
+ async def bind_prompts(
13
+ mcp: FastMCP,
14
+ # Replace with your actual use-case injection:
15
+ # prompt_usecase: SomeUseCase = Provide[Container.some_usecase],
16
+ ) -> None:
17
+ """Bind all MCP prompts to the server instance."""
18
+
19
+ @mcp.prompt()
20
+ async def example_prompt(topic: str) -> str:
21
+ """Example prompt stub — replace with your implementation."""
22
+ return f"You are an expert on {topic}. Answer concisely."
@@ -0,0 +1,22 @@
1
+ """MCP resources binding — register FastMCP resources via dependency injection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dependency_injector.wiring import Provide, inject
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from {{ project.python_package }}.application.config.container import Container
9
+
10
+
11
+ @inject
12
+ async def bind_resources(
13
+ mcp: FastMCP,
14
+ # Replace with your actual use-case injection:
15
+ # some_usecase: SomeUseCase = Provide[Container.usecase_container.some_usecase],
16
+ ) -> None:
17
+ """Bind all MCP resources to the server instance."""
18
+
19
+ @mcp.resource("file://example_resource", mime_type="application/json")
20
+ async def example_resource() -> dict[str, object]:
21
+ """Example resource stub — replace with your implementation."""
22
+ return {"message": "example resource data"}
@@ -0,0 +1,27 @@
1
+ """MCP server entrypoint — run with ``<pkg>-server`` or ``python -m <pkg>.server``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ import uvicorn
8
+
9
+ from {{ project.python_package }}.application.config.config import settings
10
+ from {{ project.python_package }}.application import app
11
+
12
+
13
+ def main() -> None:
14
+ """Start the MCP server via uvicorn.
15
+
16
+ The ASGI app is obtained from :func:`app.start_server` which wires the DI
17
+ container and mounts the FastMCP ``streamable_http_app``.
18
+ """
19
+ uvicorn.run(
20
+ app.start_server(),
21
+ host=settings.HOST,
22
+ port=settings.PORT,
23
+ )
24
+
25
+
26
+ if __name__ == "__main__":
27
+ sys.exit(main())
@@ -0,0 +1,32 @@
1
+ """Tests for {{ project.python_package }}.application.app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+ from starlette.applications import Starlette
9
+
10
+ from {{ project.python_package }}.application import app as app_module
11
+
12
+
13
+ def test_mcp_instance_is_fastmcp() -> None:
14
+ """The module-level mcp variable must be a FastMCP instance."""
15
+ assert isinstance(app_module.mcp, FastMCP)
16
+
17
+
18
+ def test_start_server_returns_starlette() -> None:
19
+ """start_server() must return a Starlette ASGI application."""
20
+ server = app_module.start_server()
21
+ assert isinstance(server, Starlette)
22
+
23
+
24
+ def test_start_server_has_routes() -> None:
25
+ """The returned Starlette app must expose at least one mounted route."""
26
+ server = app_module.start_server()
27
+ assert len(server.routes) > 0
28
+
29
+
30
+ def test_lifespan_is_callable() -> None:
31
+ """lifespan must be a callable (async context manager factory)."""
32
+ assert callable(app_module.lifespan)
@@ -0,0 +1,40 @@
1
+ """Tests for {{ project.python_package }}.infrastructure.entry_points.mcp_server.prompts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ from unittest.mock import MagicMock
8
+
9
+ from {{ project.python_package }}.infrastructure.entry_points.mcp_server.prompts import bind_prompts
10
+
11
+
12
+ def _make_mock_mcp() -> tuple[MagicMock, dict[str, object]]:
13
+ """Return (mock_mcp, registered) where registered maps prompt name → handler."""
14
+ registered: dict[str, object] = {}
15
+ mock = MagicMock()
16
+ mock.prompt = lambda: (lambda fn: registered.setdefault(fn.__name__, fn) or fn)
17
+ return mock, registered
18
+
19
+
20
+ def test_bind_prompts_is_callable() -> None:
21
+ """bind_prompts must be an async callable."""
22
+ fn = getattr(bind_prompts, "__wrapped__", bind_prompts)
23
+ assert inspect.iscoroutinefunction(fn), "bind_prompts must be async"
24
+
25
+
26
+ def test_bind_prompts_registers_example_prompt() -> None:
27
+ """Calling bind_prompts registers 'example_prompt' on the mcp instance."""
28
+ mock_mcp, registered = _make_mock_mcp()
29
+ fn = getattr(bind_prompts, "__wrapped__", bind_prompts)
30
+ asyncio.run(fn(mock_mcp))
31
+ assert "example_prompt" in registered
32
+
33
+
34
+ def test_example_prompt_returns_expected_string() -> None:
35
+ """The example_prompt inner function returns a prompt containing the topic."""
36
+ mock_mcp, registered = _make_mock_mcp()
37
+ fn = getattr(bind_prompts, "__wrapped__", bind_prompts)
38
+ asyncio.run(fn(mock_mcp))
39
+ result = asyncio.run(registered["example_prompt"]("Python")) # type: ignore[operator]
40
+ assert "Python" in result
@@ -0,0 +1,47 @@
1
+ """Tests for {{ project.python_package }}.infrastructure.entry_points.mcp_server.resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ from unittest.mock import MagicMock
8
+
9
+ from {{ project.python_package }}.infrastructure.entry_points.mcp_server.resources import bind_resources
10
+
11
+
12
+ def _make_mock_mcp() -> tuple[MagicMock, dict[str, object]]:
13
+ """Return (mock_mcp, registered) where registered maps resource URI → handler."""
14
+ registered: dict[str, object] = {}
15
+ mock = MagicMock()
16
+
17
+ def resource_decorator(uri: str, **_: object) -> object:
18
+ def wrapper(fn: object) -> object:
19
+ registered[uri] = fn
20
+ return fn
21
+ return wrapper
22
+
23
+ mock.resource = resource_decorator
24
+ return mock, registered
25
+
26
+
27
+ def test_bind_resources_is_async_callable() -> None:
28
+ """bind_resources must be an async function (or wrap one via @inject)."""
29
+ fn = getattr(bind_resources, "__wrapped__", bind_resources)
30
+ assert inspect.iscoroutinefunction(fn), "bind_resources must be async"
31
+
32
+
33
+ def test_bind_resources_registers_example_resource() -> None:
34
+ """Calling bind_resources registers 'file://example_resource' on the mcp instance."""
35
+ mock_mcp, registered = _make_mock_mcp()
36
+ fn = getattr(bind_resources, "__wrapped__", bind_resources)
37
+ asyncio.run(fn(mock_mcp))
38
+ assert "file://example_resource" in registered
39
+
40
+
41
+ def test_example_resource_returns_dict() -> None:
42
+ """The example_resource inner function returns a dict."""
43
+ mock_mcp, registered = _make_mock_mcp()
44
+ fn = getattr(bind_resources, "__wrapped__", bind_resources)
45
+ asyncio.run(fn(mock_mcp))
46
+ result = asyncio.run(registered["file://example_resource"]()) # type: ignore[operator]
47
+ assert isinstance(result, dict)