tylor-mcp 1.0.0

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 (101) hide show
  1. package/.aws-setup.sh +25 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/.mcp.json +12 -0
  4. package/AGENTS.md +93 -0
  5. package/CLAUDE.md +99 -0
  6. package/CLAUDE_PLATFORM_AWS_SETUP.md +105 -0
  7. package/LICENSE +21 -0
  8. package/README.md +146 -0
  9. package/assets/tylor_logo.png +0 -0
  10. package/assets/tylor_threads_concept.png +0 -0
  11. package/bin/tylor.js +23 -0
  12. package/hooks/kill-thread-trigger.sh +7 -0
  13. package/hooks/post-tool-use-code-index.sh +7 -0
  14. package/hooks/session-checkpoint.sh +7 -0
  15. package/hooks/session-start.sh +7 -0
  16. package/install.py +401 -0
  17. package/install.sh +260 -0
  18. package/package.json +24 -0
  19. package/pytest.ini +2 -0
  20. package/registry.json +26 -0
  21. package/server/.env.example +24 -0
  22. package/server/__init__.py +0 -0
  23. package/server/config.py +89 -0
  24. package/server/main.py +93 -0
  25. package/server/personas/analyst.md +15 -0
  26. package/server/personas/ceo.md +14 -0
  27. package/server/personas/code_agent.md +15 -0
  28. package/server/personas/cto.md +14 -0
  29. package/server/provision.py +260 -0
  30. package/server/provision_opensearch.py +154 -0
  31. package/server/requirements.txt +26 -0
  32. package/server/storage/__init__.py +0 -0
  33. package/server/storage/dynamo.py +399 -0
  34. package/server/storage/json_store.py +359 -0
  35. package/server/storage/opensearch.py +194 -0
  36. package/server/storage/s3.py +96 -0
  37. package/server/storage/tests/__init__.py +0 -0
  38. package/server/storage/tests/test_dynamo.py +452 -0
  39. package/server/storage/tests/test_json_store.py +226 -0
  40. package/server/storage/tests/test_opensearch.py +270 -0
  41. package/server/storage/tests/test_s3.py +125 -0
  42. package/server/tests/__init__.py +0 -0
  43. package/server/tests/test_install.py +606 -0
  44. package/server/tests/test_isolation.py +90 -0
  45. package/server/tests/test_ui_server.py +385 -0
  46. package/server/tests/test_ui_shader_background.py +52 -0
  47. package/server/tests/test_ui_story_6_3.py +105 -0
  48. package/server/tools/__init__.py +0 -0
  49. package/server/tools/_mcp.py +4 -0
  50. package/server/tools/agents.py +160 -0
  51. package/server/tools/ecc/__init__.py +1 -0
  52. package/server/tools/ecc/data.py +35 -0
  53. package/server/tools/ecc/diagrams.py +23 -0
  54. package/server/tools/ecc/pipeline.py +24 -0
  55. package/server/tools/ecc/presentation.py +24 -0
  56. package/server/tools/ecc/web.py +23 -0
  57. package/server/tools/executor.py +880 -0
  58. package/server/tools/harness.py +330 -0
  59. package/server/tools/help.py +162 -0
  60. package/server/tools/hooks.py +357 -0
  61. package/server/tools/personas.py +110 -0
  62. package/server/tools/registry.py +195 -0
  63. package/server/tools/router.py +117 -0
  64. package/server/tools/skill_installer.py +230 -0
  65. package/server/tools/summarizer.py +168 -0
  66. package/server/tools/tests/__init__.py +0 -0
  67. package/server/tools/tests/test_agents.py +246 -0
  68. package/server/tools/tests/test_code_index.py +108 -0
  69. package/server/tools/tests/test_ecc_tools.py +51 -0
  70. package/server/tools/tests/test_executor.py +584 -0
  71. package/server/tools/tests/test_help_agent101.py +149 -0
  72. package/server/tools/tests/test_hooks.py +124 -0
  73. package/server/tools/tests/test_kill_thread.py +125 -0
  74. package/server/tools/tests/test_new_thread_list_threads.py +293 -0
  75. package/server/tools/tests/test_personas.py +52 -0
  76. package/server/tools/tests/test_recall_memory.py +55 -0
  77. package/server/tools/tests/test_registry_client.py +308 -0
  78. package/server/tools/tests/test_router.py +263 -0
  79. package/server/tools/tests/test_skill_installer.py +174 -0
  80. package/server/tools/tests/test_switch_thread.py +163 -0
  81. package/server/tools/tests/test_thread_command_skills.py +54 -0
  82. package/server/tools/tests/test_thread_resolver.py +165 -0
  83. package/server/tools/tests/test_tier1_schema.py +296 -0
  84. package/server/tools/thread_resolver.py +75 -0
  85. package/server/tools/tylor.py +374 -0
  86. package/server/tools/ui.py +38 -0
  87. package/server/ui_server.py +292 -0
  88. package/server/validate.py +237 -0
  89. package/skills/add-skill/SKILL.md +37 -0
  90. package/skills/afk-status/SKILL.md +20 -0
  91. package/skills/bmad/SKILL.md +14 -0
  92. package/skills/help-agent101/SKILL.md +48 -0
  93. package/skills/kill-thread/SKILL.md +35 -0
  94. package/skills/list-threads/SKILL.md +35 -0
  95. package/skills/new-thread/SKILL.md +35 -0
  96. package/skills/recall/SKILL.md +39 -0
  97. package/skills/run/SKILL.md +33 -0
  98. package/skills/set-sandbox/SKILL.md +38 -0
  99. package/skills/switch-thread/SKILL.md +38 -0
  100. package/ui/claude-logo.png +0 -0
  101. package/ui/index.html +1314 -0
@@ -0,0 +1,270 @@
1
+ """
2
+ Tests for Story 2.2: OpenSearch Vector Client
3
+ Run: pytest server/storage/tests/test_opensearch.py -v
4
+ """
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from unittest.mock import MagicMock, patch
9
+
10
+ import pytest
11
+
12
+ PLUGIN_DIR = Path(__file__).parent.parent.parent.parent
13
+ sys.path.insert(0, str(PLUGIN_DIR))
14
+
15
+ from mcp.server.fastmcp.exceptions import ToolError
16
+ from server.storage.opensearch import OpenSearchClient, INDEX, TITAN_MODEL, VECTOR_DIM
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Helpers
21
+ # ---------------------------------------------------------------------------
22
+
23
+ FAKE_EMBEDDING = [0.1] * VECTOR_DIM
24
+
25
+
26
+ def make_os_client():
27
+ """Return OpenSearchClient with mocked OpenSearch + Bedrock."""
28
+ with (
29
+ patch("server.storage.opensearch.OpenSearch") as mock_os_cls,
30
+ patch("boto3.Session") as mock_session_cls,
31
+ ):
32
+ mock_os = MagicMock()
33
+ mock_os_cls.return_value = mock_os
34
+
35
+ mock_session = MagicMock()
36
+ mock_session_cls.return_value = mock_session
37
+ mock_bedrock = MagicMock()
38
+ mock_session.client.return_value = mock_bedrock
39
+
40
+ client = OpenSearchClient(host="localhost", port=9200)
41
+ client._os = mock_os
42
+ client._bedrock = mock_bedrock
43
+ return client, mock_os, mock_bedrock
44
+
45
+
46
+ def _mock_embed(mock_bedrock):
47
+ """Configure mock_bedrock to return a valid 1536-dim embedding."""
48
+ mock_body = MagicMock()
49
+ mock_body.read.return_value = json.dumps({"embedding": FAKE_EMBEDDING}).encode()
50
+ mock_bedrock.invoke_model.return_value = {"body": mock_body}
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # AC3: index_memory embeds via Titan and writes to agent-memories
55
+ # ---------------------------------------------------------------------------
56
+
57
+ def test_index_memory_calls_bedrock_with_correct_model():
58
+ client, mock_os, mock_bedrock = make_os_client()
59
+ _mock_embed(mock_bedrock)
60
+ mock_os.index.return_value = {"result": "created"}
61
+
62
+ client.index_memory(thread_id="t1", fact="The project uses FastMCP v1.")
63
+
64
+ call_kwargs = mock_bedrock.invoke_model.call_args.kwargs
65
+ assert call_kwargs["modelId"] == TITAN_MODEL
66
+ body = json.loads(call_kwargs["body"])
67
+ assert body["inputText"] == "The project uses FastMCP v1."
68
+
69
+
70
+ def test_index_memory_writes_to_agent_memories_index():
71
+ client, mock_os, mock_bedrock = make_os_client()
72
+ _mock_embed(mock_bedrock)
73
+ mock_os.index.return_value = {"result": "created"}
74
+
75
+ client.index_memory(thread_id="t1", fact="fact text")
76
+
77
+ call_kwargs = mock_os.index.call_args.kwargs
78
+ assert call_kwargs["index"] == INDEX # "agent-memories"
79
+
80
+
81
+ def test_index_memory_stores_thread_id_and_content():
82
+ client, mock_os, mock_bedrock = make_os_client()
83
+ _mock_embed(mock_bedrock)
84
+ mock_os.index.return_value = {"result": "created"}
85
+
86
+ client.index_memory(thread_id="t1", fact="important fact", metadata={"source": "user"})
87
+
88
+ doc = mock_os.index.call_args.kwargs["body"]
89
+ assert doc["thread_id"] == "t1"
90
+ assert doc["content"] == "important fact"
91
+ assert doc["source"] == "user"
92
+ assert len(doc["embedding"]) == VECTOR_DIM
93
+
94
+
95
+ def test_index_memory_returns_doc_id():
96
+ client, mock_os, mock_bedrock = make_os_client()
97
+ _mock_embed(mock_bedrock)
98
+ mock_os.index.return_value = {"result": "created"}
99
+
100
+ doc_id = client.index_memory(thread_id="t1", fact="fact")
101
+ assert isinstance(doc_id, str)
102
+ assert len(doc_id) == 32 # uuid4().hex
103
+
104
+
105
+ def test_index_memory_raises_tool_error_on_bedrock_failure():
106
+ client, mock_os, mock_bedrock = make_os_client()
107
+ mock_bedrock.invoke_model.side_effect = Exception("Bedrock throttled")
108
+
109
+ with pytest.raises(ToolError, match="embedding failed"):
110
+ client.index_memory(thread_id="t1", fact="fact")
111
+
112
+ mock_os.index.assert_not_called()
113
+
114
+
115
+ def test_index_memory_raises_tool_error_on_opensearch_failure():
116
+ client, mock_os, mock_bedrock = make_os_client()
117
+ _mock_embed(mock_bedrock)
118
+ mock_os.index.side_effect = Exception("connection refused")
119
+
120
+ with pytest.raises(ToolError, match="index_memory failed"):
121
+ client.index_memory(thread_id="t1", fact="fact")
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # AC4: search_memory returns thread-scoped results
126
+ # ---------------------------------------------------------------------------
127
+
128
+ def test_search_memory_query_includes_thread_id_filter():
129
+ client, mock_os, mock_bedrock = make_os_client()
130
+ _mock_embed(mock_bedrock)
131
+ mock_os.search.return_value = {"hits": {"hits": []}}
132
+
133
+ client.search_memory(thread_id="t1", query="project name")
134
+
135
+ os_query = mock_os.search.call_args.kwargs["body"]
136
+ bool_query = os_query["query"]["bool"]
137
+ filters = bool_query["must"]
138
+ term_filters = [f for f in filters if "term" in f]
139
+ assert any(f["term"].get("thread_id") == "t1" for f in term_filters)
140
+
141
+
142
+ def test_search_memory_query_includes_type_filter_when_provided():
143
+ client, mock_os, mock_bedrock = make_os_client()
144
+ _mock_embed(mock_bedrock)
145
+ mock_os.search.return_value = {"hits": {"hits": []}}
146
+
147
+ client.search_memory(thread_id="t1", query="SignIn", type="code_index")
148
+
149
+ os_query = mock_os.search.call_args.kwargs["body"]
150
+ filters = os_query["query"]["bool"]["must"]
151
+ assert any(f.get("term", {}).get("type") == "code_index" for f in filters)
152
+
153
+
154
+ def test_search_memory_returns_correct_shape():
155
+ client, mock_os, mock_bedrock = make_os_client()
156
+ _mock_embed(mock_bedrock)
157
+ mock_os.search.return_value = {
158
+ "hits": {
159
+ "hits": [
160
+ {
161
+ "_id": "abc123",
162
+ "_score": 0.95,
163
+ "_source": {
164
+ "thread_id": "t1",
165
+ "content": "FastMCP is great",
166
+ "created_at": "2026-05-12T10:00:00Z",
167
+ },
168
+ }
169
+ ]
170
+ }
171
+ }
172
+
173
+ results = client.search_memory(thread_id="t1", query="MCP")
174
+
175
+ assert len(results) == 1
176
+ r = results[0]
177
+ assert r["id"] == "abc123"
178
+ assert r["content"] == "FastMCP is great"
179
+ assert r["thread_id"] == "t1"
180
+ assert r["score"] == 0.95
181
+
182
+
183
+ def test_search_memory_filters_out_wrong_thread_results(caplog):
184
+ """Defence-in-depth: results with wrong thread_id are dropped at client layer."""
185
+ import logging
186
+
187
+ client, mock_os, mock_bedrock = make_os_client()
188
+ _mock_embed(mock_bedrock)
189
+ mock_os.search.return_value = {
190
+ "hits": {
191
+ "hits": [
192
+ {
193
+ "_id": "doc1",
194
+ "_score": 0.9,
195
+ "_source": {
196
+ "thread_id": "t2", # wrong thread
197
+ "content": "other thread fact",
198
+ "created_at": "2026-05-12T10:00:00Z",
199
+ },
200
+ },
201
+ {
202
+ "_id": "doc2",
203
+ "_score": 0.8,
204
+ "_source": {
205
+ "thread_id": "t1", # correct
206
+ "content": "correct fact",
207
+ "created_at": "2026-05-12T10:00:00Z",
208
+ },
209
+ },
210
+ ]
211
+ }
212
+ }
213
+
214
+ with caplog.at_level(logging.WARNING, logger="server.storage.opensearch"):
215
+ results = client.search_memory(thread_id="t1", query="something")
216
+
217
+ assert len(results) == 1
218
+ assert results[0]["content"] == "correct fact"
219
+ assert any("wrong thread_id" in r.message for r in caplog.records)
220
+
221
+
222
+ def test_search_memory_uses_k_parameter():
223
+ client, mock_os, mock_bedrock = make_os_client()
224
+ _mock_embed(mock_bedrock)
225
+ mock_os.search.return_value = {"hits": {"hits": []}}
226
+
227
+ client.search_memory(thread_id="t1", query="q", k=10)
228
+
229
+ os_query = mock_os.search.call_args.kwargs["body"]
230
+ assert os_query["size"] == 10
231
+ knn_field = os_query["query"]["bool"]["must"][0]["knn"]["embedding"]
232
+ assert knn_field["k"] == 10
233
+
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # AC5: Empty thread returns empty list
237
+ # ---------------------------------------------------------------------------
238
+
239
+ def test_search_memory_returns_empty_list_for_empty_thread():
240
+ client, mock_os, mock_bedrock = make_os_client()
241
+ _mock_embed(mock_bedrock)
242
+ mock_os.search.return_value = {"hits": {"hits": []}}
243
+
244
+ results = client.search_memory(thread_id="t1", query="anything")
245
+
246
+ assert results == []
247
+
248
+
249
+ def test_search_memory_raises_tool_error_on_opensearch_failure():
250
+ client, mock_os, mock_bedrock = make_os_client()
251
+ _mock_embed(mock_bedrock)
252
+ mock_os.search.side_effect = Exception("index not found")
253
+
254
+ with pytest.raises(ToolError, match="search_memory failed"):
255
+ client.search_memory(thread_id="t1", query="q")
256
+
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # Embedding dimension validation
260
+ # ---------------------------------------------------------------------------
261
+
262
+ def test_embed_raises_on_wrong_dimension():
263
+ client, mock_os, mock_bedrock = make_os_client()
264
+ # Return wrong-dimension embedding
265
+ mock_body = MagicMock()
266
+ mock_body.read.return_value = json.dumps({"embedding": [0.1] * 768}).encode()
267
+ mock_bedrock.invoke_model.return_value = {"body": mock_body}
268
+
269
+ with pytest.raises(ToolError, match="dimension"):
270
+ client.index_memory(thread_id="t1", fact="fact")
@@ -0,0 +1,125 @@
1
+ """
2
+ Tests for Story 2.2: S3 Blob Client
3
+ Run: pytest server/storage/tests/test_s3.py -v
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ import pytest
10
+
11
+ PLUGIN_DIR = Path(__file__).parent.parent.parent.parent
12
+ sys.path.insert(0, str(PLUGIN_DIR))
13
+
14
+ from mcp.server.fastmcp.exceptions import ToolError
15
+ from server.storage.s3 import S3Client, _s3_path
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Helpers
20
+ # ---------------------------------------------------------------------------
21
+
22
+ def make_s3_client(mock_s3=None):
23
+ with patch("boto3.Session") as mock_session_cls:
24
+ mock_session = MagicMock()
25
+ mock_session_cls.return_value = mock_session
26
+ s3_client = mock_s3 or MagicMock()
27
+ mock_session.client.return_value = s3_client
28
+ client = S3Client(bucket="agent101-blobs-123", user_id="testuser")
29
+ client._s3 = s3_client
30
+ return client, s3_client
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # AC1: put_blob stores at correct path and returns s3:// URI
35
+ # ---------------------------------------------------------------------------
36
+
37
+ def test_put_blob_returns_s3_uri():
38
+ client, s3 = make_s3_client()
39
+ s3.put_object.return_value = {}
40
+
41
+ uri = client.put_blob(thread_id="t1", key="summary", content="hello world")
42
+
43
+ assert uri == "s3://agent101-blobs-123/testuser/threads/t1/summary"
44
+
45
+
46
+ def test_put_blob_calls_put_object_with_correct_key():
47
+ client, s3 = make_s3_client()
48
+ s3.put_object.return_value = {}
49
+
50
+ client.put_blob(thread_id="t1", key="msg_001", content=b"raw bytes")
51
+
52
+ s3.put_object.assert_called_once_with(
53
+ Bucket="agent101-blobs-123",
54
+ Key="testuser/threads/t1/msg_001",
55
+ Body=b"raw bytes",
56
+ )
57
+
58
+
59
+ def test_put_blob_encodes_string_to_bytes():
60
+ client, s3 = make_s3_client()
61
+ s3.put_object.return_value = {}
62
+
63
+ client.put_blob(thread_id="t1", key="note", content="unicode: 🎉")
64
+
65
+ call_kwargs = s3.put_object.call_args.kwargs
66
+ assert isinstance(call_kwargs["Body"], bytes)
67
+ assert "unicode: 🎉".encode("utf-8") == call_kwargs["Body"]
68
+
69
+
70
+ def test_s3_path_embeds_thread_id():
71
+ path = _s3_path("u1", "thread_abc123", "summary")
72
+ assert path == "u1/threads/thread_abc123/summary"
73
+ assert "thread_abc123" in path # isolation: path encodes thread_id
74
+
75
+
76
+ def test_put_blob_raises_tool_error_on_s3_failure():
77
+ client, s3 = make_s3_client()
78
+ s3.put_object.side_effect = Exception("NoSuchBucket")
79
+
80
+ with pytest.raises(ToolError, match="put_blob failed"):
81
+ client.put_blob(thread_id="t1", key="k", content="data")
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # AC2: get_blob returns full content
86
+ # ---------------------------------------------------------------------------
87
+
88
+ def test_get_blob_returns_content():
89
+ client, s3 = make_s3_client()
90
+ mock_body = MagicMock()
91
+ mock_body.read.return_value = b"stored content here"
92
+ s3.get_object.return_value = {"Body": mock_body}
93
+
94
+ result = client.get_blob("s3://agent101-blobs-123/testuser/threads/t1/summary")
95
+
96
+ assert result == b"stored content here"
97
+
98
+
99
+ def test_get_blob_parses_uri_correctly():
100
+ client, s3 = make_s3_client()
101
+ mock_body = MagicMock()
102
+ mock_body.read.return_value = b"data"
103
+ s3.get_object.return_value = {"Body": mock_body}
104
+
105
+ client.get_blob("s3://agent101-blobs-123/testuser/threads/t1/key")
106
+
107
+ s3.get_object.assert_called_once_with(
108
+ Bucket="agent101-blobs-123",
109
+ Key="testuser/threads/t1/key",
110
+ )
111
+
112
+
113
+ def test_get_blob_raises_on_invalid_scheme():
114
+ client, s3 = make_s3_client()
115
+
116
+ with pytest.raises(ToolError, match="Invalid S3 URI scheme"):
117
+ client.get_blob("https://example.com/file")
118
+
119
+
120
+ def test_get_blob_raises_tool_error_on_s3_failure():
121
+ client, s3 = make_s3_client()
122
+ s3.get_object.side_effect = Exception("NoSuchKey")
123
+
124
+ with pytest.raises(ToolError, match="get_blob failed"):
125
+ client.get_blob("s3://bucket/key")
File without changes