Dedalus MCP implements the Model Context Protocol. Spec-faithful, minimal surface.
| What | Where |
|---|---|
| MCP spec | internals/references/modelcontextprotocol/ |
| Reference SDK | internals/references/python-sdk/ |
| Server core | src/dedalus_mcp/server/core.py |
| Versioning | src/dedalus_mcp/versioning.py |
| Types | src/dedalus_mcp/types/ |
| Tests | tests/ |
Scan wide before you write. Search for logic that already does what you need. Search for logic that could help your implementation. Understand where your contribution fits contextually within this codebase. Don't write a single line until you understand these relationships.
- Grep (or rg) the codebase for related functionality. It may already exist!
- Find the spec section for what you're implementing
- Look at similar existing code for patterns and conventions
- Identify code that your implementation should integrate with
Write tests first. This project follows TDD. Don't implement features in a vacuum—define what "correct" looks like before you write production code.
The workflow:
-
Create or update an execspec — For any significant work, maintain a local
EXECSPEC-{topic}.mdindocs/dedalus_mcp/. This is your working document: what you're building, what invariants must hold, what's done, what's next. -
Write a failing test — Before implementing, write a test that captures an invariant from your execspec. The test should fail because the feature doesn't exist yet.
-
Make it pass — Write the minimal code to make the test pass. No more.
-
Refactor — Clean up while tests are green.
-
Repeat — Each passing test is a small victory. Accumulate these toward the full vision.
Example execspec structure:
# EXECSPEC: Elicitation Support
## Goal
Implement client-side elicitation per MCP 2025-06-18.
## Invariants
- [ ] `elicit()` raises if server doesn't advertise elicitation capability
- [ ] Timeout triggers `ElicitationTimeoutError`
- [ ] Response schema is validated against `ElicitationResult`
## Progress
- [x] Basic request/response flow
- [ ] Timeout handling
- [ ] Schema validationWhy this matters: Tests are executable documentation. When you write test_elicit_raises_without_capability(), you're stating a contract. Future changes that break this contract will fail loudly. The execspec keeps you honest—it's easy to lose sight of the goal mid-implementation.
See docs/dedalus_mcp/EXECSPEC*.md for examples of how we track larger features.
Fail loudly. Never silently degrade. Unknown enum? Raise. Unsupported version? Raise. Invalid config? Raise.
# Bad
def get_version(v: str) -> str:
return VERSIONS.get(v, "2025-06-18") # Silent fallback
# Good
def get_version(v: str) -> str:
if v not in VERSIONS:
raise UnsupportedProtocolVersionError(v)
return VERSIONS[v]Silent fallbacks create bugs that "work" until production. Explicit failures are debuggable.
Cite the spec. Link to the spec clause in docstrings or comments.
async def list_tools(self) -> ListToolsResult:
"""List available tools.
See: docs/mcp/spec/schema-reference/tools-list.md
"""Minimal dependencies. This project respects developers' dependency budgets. Every dependency is a burden on downstream users—install weight, security surface, version conflicts. Don't add packages unless absolutely necessary.
Core deps: mcp, pydantic. Everything else is optional. Before adding a dependency:
- Can you implement it in <50 lines? Do that instead.
- Is it only needed for one feature? Make it an optional extra.
- Is it a dev/test dependency? Keep it out of the main install.
Prefer enums over constants. Use Enum/StrEnum/IntEnum instead of module-level constants or raw dicts. Enums give you type safety, autocomplete, exhaustiveness checking in match statements, and self-documentation. See docs/style/best-practices.md for examples.
Decorators attach metadata. collect() registers.
from dedalus_mcp import MCPServer, tool
@tool(description="Add numbers")
def add(a: int, b: int) -> int:
return a + b
server = MCPServer("math")
server.collect(add)Same function can register to multiple servers. No global state.
Dedalus MCP is a temporal inscription of the MCP spec—it respects that the protocol evolves over time. Every feature has a version where it originated, and our code must reflect this.
Before implementing any feature, ask:
- Which MCP version introduced this feature?
- Does it exist in earlier versions? If so, where did it originate?
- Is this a version-specific change, or does it span multiple versions?
Protocol versions are tracked in src/dedalus_mcp/versioning.py. Check docs/dedalus_mcp/version-matrix.md to see what exists where.
When adding version-specific behavior:
from dedalus_mcp.versioning import current_profile, FeatureId
profile = current_profile()
if profile.supports(FeatureId.PROGRESS_MESSAGE_FIELD):
message = "Processing..."
else:
message = NoneThe SDK uses exclude_none=True. Set unsupported fields to None.
Adding a new feature:
- Find the origin version — Read the spec changelogs to determine when the feature was introduced
- Register it correctly — Add to
FEATURE_REGISTRYwith the correctadded=version - Consider backwards compatibility — If the feature builds on earlier concepts, ensure those are also tracked
- Test across versions — Write tests that verify the feature is absent in earlier versions and present in later ones
Adding a new MCP version:
- Read changelog at
internals/references/modelcontextprotocol/docs/specification/{version}/changelog.mdx - Add
V_YYYY_MM_DDconstant - Add
FeatureIdentries with correctadded=dates - Write migration function
- Add tests in
tests/protocol_versions/{version}/
Why this matters: Users negotiate protocol versions with servers. If Dedalus MCP claims to support 2024-11-05 but includes features from 2025-06-18, we break interoperability. The versioning system is our contract with the spec.
Tests validate spec guarantees. Run:
uv run pytest tests/ -qEach test should focus on one protocol behavior. Name tests after what they verify.
- One concept per file
- Services in
src/dedalus_mcp/server/services/ - Types re-exported from
src/dedalus_mcp/types/ - Examples in
examples/
Two official transports: STDIO and Streamable HTTP. Default is Streamable HTTP.
await server.serve() # HTTP on :8000
await server.serve(transport="stdio") # STDIO- Check the MCP spec
- Look at existing implementations
- Fail explicitly rather than guess
- Keep it minimal