Skip to content
Merged
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
139 changes: 138 additions & 1 deletion packages/mcp/src/sagasmith_coc_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,6 @@
"branches",
"campaigns",
"characters",
"constraints",
"conversations",
"events",
"history",
Expand All @@ -413,6 +412,142 @@
}


def _continuity_context_output_schema(properties: dict[str, Any]) -> dict[str, Any]:
"""Describe every authoritative continuity projection without weakening validation."""

string_list = {
"type": "array",
"maxItems": 1_000,
"items": {"type": "string"},
"uniqueItems": True,
}
actor_memory_item = {
"type": "object",
"required": ["basis_ref", "source", "content", "refs", "record", "score"],
"properties": {
"basis_ref": {"type": "string", "minLength": 1},
"source": {"enum": ["actor_state", "actor_knowledge", "event"]},
"content": {"type": "string"},
"refs": deepcopy(string_list),
"record": {"type": "object"},
"score": {"type": "integer"},
},
"additionalProperties": False,
}
actor_memory = {
"type": "object",
"required": ["identity", "motivational", "semantic", "episodic", "diagnostics"],
"properties": {
name: {
"type": "array",
"maxItems": 1_000,
"items": deepcopy(actor_memory_item),
}
for name in ("identity", "motivational", "semantic", "episodic")
}
| {"diagnostics": {"type": "object"}},
"additionalProperties": False,
}
properties.update(
{
"schema_version": {"type": "integer", "const": 1},
"purpose": {
"type": "string",
"enum": [
"actor_memory",
"actor_turn",
"audience_render",
"faction_turn",
"campaign_expansion",
"source_interpretation",
"bounded_ruling",
],
},
"actor_id": {"type": "string"},
"memory": actor_memory,
"branch": {"type": "object"},
"facts": {"type": "array", "maxItems": 100, "items": {"type": "object"}},
"events": {"type": "array", "maxItems": 100, "items": {"type": "object"}},
"actor_knowledge": {
"type": "array",
"maxItems": 100,
"items": {"type": "object"},
},
"module_evidence": {
"type": "array",
"maxItems": 1_000,
"items": {"type": "object"},
},
"scoped_scene": {"type": ["object", "null"]},
"retrieval": {"type": "object"},
"subject": {
"type": "object",
"required": ["kind", "id", "name"],
"properties": {
"kind": {"type": "string"},
"id": {"type": "string"},
"name": {"type": "string"},
},
"additionalProperties": False,
},
"stimulus": {"type": ["object", "null"]},
"context": {"type": "object"},
"constraints": {
"type": "object",
"required": [
"allowed_basis_refs",
"allowed_target_refs",
"may_roll_dice",
"may_call_tools",
"may_write_state",
"output_contract",
],
"properties": {
"allowed_basis_refs": deepcopy(string_list),
"allowed_target_refs": deepcopy(string_list),
"may_roll_dice": {"type": "boolean", "const": False},
"may_call_tools": {"type": "boolean", "const": False},
"may_write_state": {"type": "boolean", "const": False},
"output_contract": {
"type": "string",
"enum": sorted(BOUNDED_OUTPUT_CONTRACTS.values()),
},
},
"additionalProperties": False,
},
"delegation": {
"type": "object",
"required": [
"schema_version",
"task",
"execution",
"inherit_agent_history",
"tools_exposed",
"persist_worker_session",
"authoritative_result",
],
"properties": {
"schema_version": {"type": "integer", "const": 1},
"task": {"type": "string", "pattern": "^propose_"},
"execution": {"type": "string", "const": "awaited_fresh_context"},
"inherit_agent_history": {"type": "boolean", "const": False},
"tools_exposed": {"type": "boolean", "const": False},
"persist_worker_session": {"type": "boolean", "const": False},
"authoritative_result": {"type": "boolean", "const": False},
},
"additionalProperties": False,
},
"bundle_receipt": {"type": "object"},
}
)
return {
"type": "object",
"description": "Structured authoritative result for the continuity_context tool.",
"properties": properties,
"additionalProperties": False,
}


def _argument_error(code: str, message: str, *, retryable: bool, recovery: str) -> dict[str, Any]:
return {
"error": {"code": code, "message": message, "retryable": retryable, "recovery": recovery}
Expand Down Expand Up @@ -559,6 +694,8 @@ def _output_schema(tool_name: str) -> dict[str, Any]:
},
"additionalProperties": False,
}
if tool_name == "continuity_context":
return _continuity_context_output_schema(properties)
return {
"type": "object",
"description": f"Structured authoritative result for the {tool_name} tool.",
Expand Down
49 changes: 49 additions & 0 deletions packages/mcp/tests/test_collection_pagination_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from dataclasses import dataclass
from pathlib import Path

from jsonschema import Draft202012Validator
from mcp import Client, StdioServerParameters
from sagasmith_core import RevisionService

Expand Down Expand Up @@ -171,6 +172,54 @@ async def exercise() -> None:
asyncio.run(exercise())


def test_real_modern_client_validates_every_continuity_result_shape(tmp_path: Path) -> None:
async def exercise() -> None:
campaign_id, actor_id = await _seed_campaign(create_server(_config(tmp_path)))
parameters = StdioServerParameters(
command=sys.executable,
args=["-m", "sagasmith_coc_mcp.server"],
env=_environment(tmp_path),
)
async with Client(parameters, mode="2026-07-28") as client:
catalog = {tool.name: tool for tool in (await client.list_tools()).tools}
schema = catalog["continuity_context"].output_schema
assert schema is not None
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)

results = [
await client.call_tool("continuity_context", {"campaign_id": campaign_id}),
await client.call_tool(
"continuity_context",
{
"campaign_id": campaign_id,
"actor_id": actor_id,
"purpose": "actor_memory",
},
),
await client.call_tool(
"continuity_context",
{
"campaign_id": campaign_id,
"purpose": "source_interpretation",
"query": "Interpret the lamp's soot against known evidence.",
},
),
]
for result in results:
assert result.is_error is False, result.content
assert result.structured_content is not None
assert list(validator.iter_errors(result.structured_content)) == []

actor_memory = results[1].structured_content
assert actor_memory["memory"]["identity"]
bounded = results[2].structured_content
assert bounded["constraints"]["may_write_state"] is False
assert schema["properties"]["constraints"]["type"] == "object"

asyncio.run(exercise())


def test_event_cursor_reaches_records_beyond_first_hundred(tmp_path: Path) -> None:
async def exercise() -> None:
server = create_server(_config(tmp_path))
Expand Down