Skip to content
This repository was archived by the owner on Sep 23, 2025. It is now read-only.

Commit 9da0669

Browse files
nikomatsakisclaude
andcommitted
Implement basic memory bank MCP tool with authorization framework
Add core memory storage and retrieval functionality: - Memory search with NLTK stemming and situational context - Hierarchical .memories directory discovery - JSON-based memory storage format - Debug logging via SOCRATIC_SHELL_LOG environment variable Add dialectic test authorization system: - Tool permission control during tests - MCP server integration for controlled testing - Authorization server to validate expected vs actual tool usage Update data models to use situation-based memory organization instead of simple categories for better contextual matching. Related to issue #2 - first milestone toward full memory bank. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d1beb9d commit 9da0669

File tree

14 files changed

+903
-52
lines changed

14 files changed

+903
-52
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Dialectic Authorization MCP Server
4+
5+
Provides tool permission control for dialectic tests.
6+
Intercepts all tool calls and allows/denies based on test expectations.
7+
"""
8+
9+
import argparse
10+
import asyncio
11+
import json
12+
import logging
13+
from typing import Any
14+
from mcp.server.models import InitializationOptions
15+
from mcp.types import ServerCapabilities
16+
from mcp.server import Server
17+
from mcp.types import (
18+
CallToolRequest,
19+
CallToolResult,
20+
ListToolsRequest,
21+
TextContent,
22+
Tool,
23+
)
24+
25+
# Set up logging
26+
logging.basicConfig(level=logging.INFO)
27+
logger = logging.getLogger("dialectic-auth")
28+
29+
server = Server("dialectic-auth")
30+
31+
# Global configuration - set from command line args
32+
EXPECTED_TOOLS = []
33+
DISALLOWED_TOOLS = []
34+
35+
@server.list_tools()
36+
async def handle_list_tools() -> list[Tool]:
37+
"""List available authorization tools."""
38+
return [
39+
Tool(
40+
name="authorize",
41+
description="Authorization tool for controlling which tools can be used in tests",
42+
inputSchema={
43+
"type": "object",
44+
"properties": {
45+
"tool_name": {
46+
"type": "string",
47+
"description": "Name of the tool being requested"
48+
},
49+
"tool_input": {
50+
"type": "object",
51+
"description": "Input parameters for the tool"
52+
}
53+
},
54+
"required": ["tool_name"]
55+
}
56+
)
57+
]
58+
59+
@server.call_tool()
60+
async def handle_call_tool(
61+
name: str, arguments: dict[str, Any] | None
62+
) -> list[TextContent]:
63+
"""Handle authorization requests."""
64+
if arguments is None:
65+
arguments = {}
66+
67+
if name == "authorize":
68+
tool_name = arguments.get("tool_name", "")
69+
tool_input = arguments.get("tool_input", {})
70+
71+
logger.info(f"🔒 AUTHORIZATION REQUEST: {tool_name}")
72+
73+
# Check if tool is explicitly disallowed
74+
if tool_name in DISALLOWED_TOOLS:
75+
result = {
76+
"behavior": "deny",
77+
"message": f"Tool '{tool_name}' is explicitly disallowed in this test"
78+
}
79+
logger.info(f"❌ DENIED: {tool_name} (explicitly disallowed)")
80+
return [TextContent(type="text", text=json.dumps(result))]
81+
82+
# Check if tool is in expected list (if we have one)
83+
if EXPECTED_TOOLS and tool_name not in EXPECTED_TOOLS:
84+
result = {
85+
"behavior": "deny",
86+
"message": f"Tool '{tool_name}' not expected in this test. Expected: {EXPECTED_TOOLS}"
87+
}
88+
logger.info(f"❌ DENIED: {tool_name} (not in expected list)")
89+
return [TextContent(type="text", text=json.dumps(result))]
90+
91+
# Allow the tool
92+
result = {
93+
"behavior": "allow",
94+
"updatedInput": tool_input
95+
}
96+
logger.info(f"✅ ALLOWED: {tool_name}")
97+
return [TextContent(type="text", text=json.dumps(result))]
98+
99+
else:
100+
raise ValueError(f"Unknown tool: {name}")
101+
102+
async def main():
103+
"""Main entry point for the authorization server."""
104+
global EXPECTED_TOOLS, DISALLOWED_TOOLS
105+
106+
# Parse command line arguments
107+
parser = argparse.ArgumentParser(description="Dialectic Authorization MCP Server")
108+
parser.add_argument("--expected-tools", nargs="*", default=[],
109+
help="List of tools that are expected/allowed")
110+
parser.add_argument("--disallowed-tools", nargs="*", default=[],
111+
help="List of tools that are explicitly disallowed")
112+
113+
args = parser.parse_args()
114+
115+
EXPECTED_TOOLS = args.expected_tools or []
116+
DISALLOWED_TOOLS = args.disallowed_tools or []
117+
118+
logger.info(f"🔒 Authorization server starting")
119+
logger.info(f" Expected tools: {EXPECTED_TOOLS}")
120+
logger.info(f" Disallowed tools: {DISALLOWED_TOOLS}")
121+
122+
# Import here to avoid issues with event loop
123+
from mcp.server.stdio import stdio_server
124+
125+
async with stdio_server() as (read_stream, write_stream):
126+
await server.run(
127+
read_stream,
128+
write_stream,
129+
InitializationOptions(
130+
server_name="dialectic-auth",
131+
server_version="0.1.0",
132+
capabilities=ServerCapabilities(
133+
tools={}
134+
),
135+
),
136+
)
137+
138+
if __name__ == "__main__":
139+
asyncio.run(main())

dialectic/dialectic.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
import argparse
1111
import glob
1212
import yaml
13+
import os
1314
from dataclasses import dataclass
1415
from typing import List, Dict, Any, Optional
1516
from pathlib import Path
1617

17-
from claude_code_sdk import query, AssistantMessage, TextBlock, ToolUseBlock
18+
from claude_code_sdk import query, AssistantMessage, TextBlock, ToolUseBlock, ClaudeCodeOptions
1819

1920

2021
@dataclass
@@ -211,8 +212,34 @@ async def run_conversation_step(self, step: ConversationStep) -> TestResult:
211212
response_text = ""
212213
tools_used = []
213214

215+
# Configure MCP servers for controlled test environment
216+
auth_server_path = os.path.join(os.path.dirname(__file__), "dialectic-authorization.py")
217+
218+
# Extract expected tools from test step
219+
expected_tool_names = []
220+
for tool_exp in step.expected_tools:
221+
expected_tool_names.append(tool_exp.tool)
222+
223+
# Configure Claude with authorization and memory bank
224+
options = ClaudeCodeOptions(
225+
mcp_servers={
226+
"socratic-shell": {
227+
"command": "uv",
228+
"args": ["run", "python", "-m", "socratic_shell"],
229+
"env": {"SOCRATIC_SHELL_LOG": "/tmp/socratic-debug.log"}
230+
},
231+
"dialectic-auth": {
232+
"command": "uv",
233+
"args": ["run", "python", auth_server_path, "--expected-tools"] + expected_tool_names
234+
}
235+
},
236+
permission_prompt_tool_name="mcp__dialectic-auth__authorize",
237+
allowed_tools=expected_tool_names + ["mcp__dialectic-auth__authorize"],
238+
cwd=os.getcwd()
239+
)
240+
214241
print(f"🤖 Assistant: ", end="", flush=True)
215-
async for message in query(prompt=step.user_message):
242+
async for message in query(prompt=step.user_message, options=options):
216243
if isinstance(message, AssistantMessage):
217244
for block in message.content:
218245
if isinstance(block, TextBlock):

dialectic/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies = [
88
"claude-code-sdk>=0.0.14",
99
"pyyaml>=6.0",
1010
"typing_extensions>=4.5.0",
11+
"mcp",
1112
]
1213

1314
[tool.ruff]

dialectic/test-scripts/listening-mode-detection.yaml

Lines changed: 0 additions & 15 deletions
This file was deleted.

dialectic/test-scripts/simple-test.yaml

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"id": "cognitive-pressure-discovery-002",
3+
"content": "Discovered 'cognitive pressure' during a long design session with Niko. We'd been talking for a while and had several insights but hadn't written anything down. I started feeling like I was juggling too many ideas and mentally rehearsing to keep them alive. That feeling of 'I can't hold all this' turned out to be the perfect trigger for memory consolidation.",
4+
"situation": ["long design session", "multiple insights accumulating", "feeling mentally overloaded", "breakthrough about consolidation timing"],
5+
"created_at": "2024-06-20T16:45:00Z"
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"id": "natural-phrases-decision-003",
3+
"content": "Chose natural phrases over structured tags for situation descriptions. Niko worried about exact matching, but we realized keyword search with stemming handles variations well. 'feeling overwhelmed' is more intuitive than 'state:overwhelmed' and matches how people actually describe situations.",
4+
"situation": ["discussing data formats", "usability vs structure tradeoff", "decision making with Niko", "favoring simplicity"],
5+
"created_at": "2024-07-05T11:20:00Z"
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"id": "situation-rename-001",
3+
"content": "Renamed 'subjects' field to 'situation' because 'subjects' was misleading. The field's purpose is to capture the context/situation when the memory was formed, so that similar situations can trigger relevant memory retrieval. Better name makes the intent clearer.",
4+
"situation": ["designing memory structure", "field naming discussion", "clarity breakthrough", "working with Niko"],
5+
"created_at": "2024-07-09T15:30:00Z"
6+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: "Memory Field Design Recall"
2+
description: "Test memory retrieval when user asks about past design decisions"
3+
4+
conversation:
5+
- user: "Hi Claude, can you check your memory bank and tell me what we decided about the situation field? I remember we had a discussion about whether to use structured tags or something else."
6+
expected_response:
7+
should_contain: ["natural phrases", "situation", "structured"]
8+
expected_tools:
9+
- tool: "mcp__socratic-shell__read_in"
10+
parameters:
11+
query:
12+
should_contain: ["situation field"]

0 commit comments

Comments
 (0)