@gethmy/mcp 2.5.0 → 2.5.1

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.
@@ -1,321 +0,0 @@
1
- /**
2
- * Integration tests for the Intelligent Agent Memory System.
3
- *
4
- * These tests exercise the MCP tools end-to-end via the Harmony API.
5
- * Prerequisites:
6
- * 1. `supabase db push` (migration applied)
7
- * 2. MCP server configured with valid API key
8
- * 3. Active workspace and project set
9
- *
10
- * Run with: bun test packages/mcp-server/src/__tests__/integration-memory-system.test.ts
11
- *
12
- * These tests create real entities and clean them up afterwards.
13
- * If a test fails mid-way, you may need to manually delete leftover entities.
14
- */
15
-
16
- import { afterAll, describe, expect, test } from "bun:test";
17
- import { getClient } from "../api-client.js";
18
- import {
19
- getActiveProjectId,
20
- getActiveWorkspaceId,
21
- isConfigured,
22
- loadConfig,
23
- } from "../config.js";
24
-
25
- // Track entities created during tests for cleanup
26
- const createdEntityIds: string[] = [];
27
-
28
- // Skip all tests if MCP server isn't configured
29
- const configured = (() => {
30
- try {
31
- loadConfig();
32
- return isConfigured() && !!getActiveWorkspaceId();
33
- } catch {
34
- return false;
35
- }
36
- })();
37
-
38
- // Check if the tier migration has been applied by testing a create
39
- const migrationApplied = await (async () => {
40
- if (!configured) return false;
41
- try {
42
- const client = getClient();
43
- const result = await client.createMemoryEntity({
44
- workspace_id: getActiveWorkspaceId()!,
45
- project_id: getActiveProjectId() || undefined,
46
- type: "context",
47
- scope: getActiveProjectId() ? "project" : "workspace",
48
- memory_tier: "draft",
49
- title: "[TEST] Migration check - delete me",
50
- content: "Checking if memory_tier column exists.",
51
- agent_identifier: "test-runner",
52
- });
53
- const entity = result.entity as Record<string, unknown>;
54
- const hasTier = entity.memory_tier === "draft";
55
- // Clean up probe entity
56
- if (entity.id) {
57
- await client.deleteMemoryEntity(entity.id as string).catch(() => {});
58
- }
59
- return hasTier;
60
- } catch {
61
- return false;
62
- }
63
- })();
64
-
65
- const describeIf = configured ? describe : describe.skip;
66
- const describeIfMigrated =
67
- configured && migrationApplied ? describe : describe.skip;
68
-
69
- // Cleanup: delete all test entities
70
- afterAll(async () => {
71
- if (!configured || createdEntityIds.length === 0) return;
72
- const client = getClient();
73
- for (const id of createdEntityIds) {
74
- try {
75
- await client.deleteMemoryEntity(id);
76
- } catch {
77
- // Entity may already be deleted
78
- }
79
- }
80
- });
81
-
82
- describeIfMigrated("Memory Tier System (integration)", () => {
83
- test("create entity with explicit tier", async () => {
84
- const client = getClient();
85
- const workspaceId = getActiveWorkspaceId()!;
86
- const projectId = getActiveProjectId() || undefined;
87
-
88
- const result = await client.createMemoryEntity({
89
- workspace_id: workspaceId,
90
- project_id: projectId,
91
- type: "context",
92
- scope: projectId ? "project" : "workspace",
93
- memory_tier: "draft",
94
- title: "[TEST] Draft memory tier test",
95
- content: "This is a test draft memory.",
96
- tags: ["test", "integration"],
97
- agent_identifier: "test-runner",
98
- });
99
-
100
- const entity = result.entity as Record<string, unknown>;
101
- expect(entity.id).toBeTruthy();
102
- expect(entity.memory_tier).toBe("draft");
103
- createdEntityIds.push(entity.id as string);
104
- });
105
-
106
- test("create entity with default tier based on type", async () => {
107
- const client = getClient();
108
- const workspaceId = getActiveWorkspaceId()!;
109
- const projectId = getActiveProjectId() || undefined;
110
-
111
- // 'lesson' type should default to 'episode' tier
112
- const result = await client.createMemoryEntity({
113
- workspace_id: workspaceId,
114
- project_id: projectId,
115
- type: "lesson",
116
- scope: projectId ? "project" : "workspace",
117
- title: "[TEST] Lesson tier default",
118
- content: "Testing that lessons default to episode tier.",
119
- tags: ["test", "integration"],
120
- agent_identifier: "test-runner",
121
- });
122
-
123
- const entity = result.entity as Record<string, unknown>;
124
- expect(entity.id).toBeTruthy();
125
- expect(entity.memory_tier).toBe("episode");
126
- createdEntityIds.push(entity.id as string);
127
- });
128
-
129
- test("create pattern defaults to reference tier", async () => {
130
- const client = getClient();
131
- const workspaceId = getActiveWorkspaceId()!;
132
- const projectId = getActiveProjectId() || undefined;
133
-
134
- const result = await client.createMemoryEntity({
135
- workspace_id: workspaceId,
136
- project_id: projectId,
137
- type: "pattern",
138
- scope: projectId ? "project" : "workspace",
139
- title: "[TEST] Pattern tier default",
140
- content: "Testing that patterns default to reference tier.",
141
- tags: ["test", "integration"],
142
- agent_identifier: "test-runner",
143
- });
144
-
145
- const entity = result.entity as Record<string, unknown>;
146
- expect(entity.memory_tier).toBe("reference");
147
- createdEntityIds.push(entity.id as string);
148
- });
149
-
150
- test("update entity tier via promote", async () => {
151
- const client = getClient();
152
- const workspaceId = getActiveWorkspaceId()!;
153
- const projectId = getActiveProjectId() || undefined;
154
-
155
- // Create a draft
156
- const createResult = await client.createMemoryEntity({
157
- workspace_id: workspaceId,
158
- project_id: projectId,
159
- type: "context",
160
- scope: projectId ? "project" : "workspace",
161
- memory_tier: "draft",
162
- title: "[TEST] Promotion test draft",
163
- content: "Draft that will be promoted.",
164
- tags: ["test", "integration"],
165
- agent_identifier: "test-runner",
166
- });
167
-
168
- const entityId = (createResult.entity as Record<string, unknown>)
169
- .id as string;
170
- createdEntityIds.push(entityId);
171
-
172
- // Promote to episode
173
- const updateResult = await client.updateMemoryEntity(entityId, {
174
- memory_tier: "episode",
175
- metadata: {
176
- promoted_from_tier: "draft",
177
- promotion_reason: "test promotion",
178
- promoted_at: new Date().toISOString(),
179
- },
180
- });
181
-
182
- const updated = updateResult.entity as Record<string, unknown>;
183
- expect(updated.memory_tier).toBe("episode");
184
- });
185
- });
186
-
187
- describeIf("Context Assembly (integration)", () => {
188
- test("search returns entities with tier info", async () => {
189
- const client = getClient();
190
- const workspaceId = getActiveWorkspaceId()!;
191
- const projectId = getActiveProjectId() || undefined;
192
-
193
- // Create a searchable entity
194
- const createResult = await client.createMemoryEntity({
195
- workspace_id: workspaceId,
196
- project_id: projectId,
197
- type: "solution",
198
- scope: projectId ? "project" : "workspace",
199
- memory_tier: "reference",
200
- title: "[TEST] Unique search term xyzzy42",
201
- content: "A solution for testing context assembly search.",
202
- confidence: 0.95,
203
- tags: ["test", "context-assembly"],
204
- agent_identifier: "test-runner",
205
- });
206
-
207
- const entityId = (createResult.entity as Record<string, unknown>)
208
- .id as string;
209
- createdEntityIds.push(entityId);
210
-
211
- // Search for it
212
- const searchResult = await client.searchMemoryEntities(
213
- workspaceId,
214
- "xyzzy42",
215
- { project_id: projectId, limit: 5 },
216
- );
217
-
218
- expect(searchResult.entities.length).toBeGreaterThan(0);
219
- const found = searchResult.entities.find(
220
- (e: unknown) => (e as Record<string, unknown>).id === entityId,
221
- );
222
- expect(found).toBeTruthy();
223
- expect((found as Record<string, unknown>).memory_tier).toBe("reference");
224
- });
225
- });
226
-
227
- describeIf("Graph Walk (integration)", () => {
228
- test("create entities with relations and traverse", async () => {
229
- const client = getClient();
230
- const workspaceId = getActiveWorkspaceId()!;
231
- const projectId = getActiveProjectId() || undefined;
232
- const scope = projectId ? "project" : "workspace";
233
-
234
- // Create two related entities
235
- const errorResult = await client.createMemoryEntity({
236
- workspace_id: workspaceId,
237
- project_id: projectId,
238
- type: "error",
239
- scope,
240
- memory_tier: "reference",
241
- title: "[TEST] Graph walk error",
242
- content: "An error for graph walk testing.",
243
- tags: ["test", "graph-walk"],
244
- agent_identifier: "test-runner",
245
- });
246
- const errorId = (errorResult.entity as Record<string, unknown>)
247
- .id as string;
248
- createdEntityIds.push(errorId);
249
-
250
- const solutionResult = await client.createMemoryEntity({
251
- workspace_id: workspaceId,
252
- project_id: projectId,
253
- type: "solution",
254
- scope,
255
- memory_tier: "reference",
256
- title: "[TEST] Graph walk solution",
257
- content: "A solution for graph walk testing.",
258
- tags: ["test", "graph-walk"],
259
- agent_identifier: "test-runner",
260
- });
261
- const solutionId = (solutionResult.entity as Record<string, unknown>)
262
- .id as string;
263
- createdEntityIds.push(solutionId);
264
-
265
- // Create relation
266
- const relResult = await client.createMemoryRelation({
267
- source_id: errorId,
268
- target_id: solutionId,
269
- relation_type: "resolved_by",
270
- confidence: 1.0,
271
- });
272
- expect((relResult.relation as Record<string, unknown>).id).toBeTruthy();
273
-
274
- // Verify relations via getRelatedEntities
275
- const related = await client.getRelatedEntities(errorId);
276
- expect(related.outgoing.length).toBeGreaterThan(0);
277
-
278
- // API returns relations with embedded target entity, not flat target_id
279
- const outRel = related.outgoing.find((r: unknown) => {
280
- const rel = r as Record<string, unknown>;
281
- const target = rel.target as Record<string, unknown> | undefined;
282
- return target?.id === solutionId || rel.target_id === solutionId;
283
- });
284
- expect(outRel).toBeTruthy();
285
- });
286
- });
287
-
288
- describeIf("Vault Index with Tiers (integration)", () => {
289
- test("vault index includes tier information", async () => {
290
- const client = getClient();
291
- const workspaceId = getActiveWorkspaceId()!;
292
- const projectId = getActiveProjectId() || undefined;
293
-
294
- let result: { entities: unknown[]; count: number };
295
- try {
296
- result = await client.getVaultIndex({
297
- workspace_id: workspaceId,
298
- project_id: projectId,
299
- limit: 10,
300
- });
301
- } catch (e) {
302
- // Endpoint may not be deployed yet - skip gracefully
303
- const msg = e instanceof Error ? e.message : String(e);
304
- if (msg.includes("Unknown endpoint")) {
305
- console.log("Skipping: vault index endpoint not deployed yet");
306
- return;
307
- }
308
- throw e;
309
- }
310
-
311
- expect(result.entities).toBeDefined();
312
- // If there are entities, check they have tier info
313
- if (result.entities.length > 0) {
314
- const first = result.entities[0] as Record<string, unknown>;
315
- // memory_tier might be present if migration is applied
316
- if (first.memory_tier !== undefined) {
317
- expect(["draft", "episode", "reference"]).toContain(first.memory_tier);
318
- }
319
- }
320
- });
321
- });
@@ -1,141 +0,0 @@
1
- /**
2
- * In-process MCP integration smoke. Card #182.
3
- *
4
- * Wires a real Server through `registerHandlers` and a real Client over an
5
- * InMemoryTransport pair. Asserts JSON-RPC wire format end-to-end:
6
- *
7
- * - listTools returns every TOOLS row.
8
- * - callTool dispatches and returns content.
9
- * - listResources / readResource roundtrip.
10
- * - unknown tool name surfaces as MCP-level error.
11
- *
12
- * This is the only place we exercise the SDK's transport + JSON-RPC layer.
13
- * The dispatch unit tests in tool-dispatch.test.ts cover handler logic with
14
- * a fake Server; this file confirms nothing breaks at the wire boundary.
15
- *
16
- * Run with: bun test packages/mcp-server/src/__tests__/mcp-integration.test.ts
17
- */
18
-
19
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
20
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
21
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
22
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
23
-
24
- import {
25
- RESOURCES,
26
- registerHandlers,
27
- TOOLS,
28
- type ToolDeps,
29
- } from "../server.js";
30
-
31
- const TOOL_COUNT = Object.keys(TOOLS).length;
32
-
33
- function makeDeps(): ToolDeps {
34
- return {
35
- getClient: () => ({}) as never,
36
- isConfigured: () => true,
37
- getActiveProjectId: () => "11111111-1111-1111-1111-111111111111",
38
- getActiveWorkspaceId: () => "22222222-2222-2222-2222-222222222222",
39
- setActiveProject: () => {},
40
- setActiveWorkspace: () => {},
41
- getApiUrl: () => "http://localhost",
42
- getMemoryDir: () => null,
43
- getUserEmail: () => null,
44
- saveConfig: () => {},
45
- resetClient: () => {},
46
- };
47
- }
48
-
49
- describe("MCP integration — in-process Client ↔ Server", () => {
50
- let client: Client;
51
- let server: Server;
52
-
53
- beforeAll(async () => {
54
- const [clientTransport, serverTransport] =
55
- InMemoryTransport.createLinkedPair();
56
-
57
- server = new Server(
58
- { name: "harmony-test", version: "0.0.0" },
59
- { capabilities: { tools: {}, resources: {} } },
60
- );
61
- registerHandlers(server, makeDeps());
62
- await server.connect(serverTransport);
63
-
64
- client = new Client(
65
- { name: "harmony-test-client", version: "0.0.0" },
66
- { capabilities: {} },
67
- );
68
- await client.connect(clientTransport);
69
- });
70
-
71
- afterAll(async () => {
72
- await client.close();
73
- await server.close();
74
- });
75
-
76
- test("listTools delivers every registered tool", async () => {
77
- const result = await client.listTools();
78
- expect(result.tools.length).toBe(TOOL_COUNT);
79
- for (const tool of result.tools) {
80
- expect(typeof tool.name).toBe("string");
81
- expect(typeof tool.description).toBe("string");
82
- expect(tool.inputSchema).toMatchObject({ type: "object" });
83
- }
84
- });
85
-
86
- test("callTool routes harmony_get_context end-to-end", async () => {
87
- const result = await client.callTool({
88
- name: "harmony_get_context",
89
- arguments: {},
90
- });
91
-
92
- expect(result.isError).toBeUndefined();
93
- const content = result.content as Array<{ type: string; text: string }>;
94
- expect(content[0].type).toBe("text");
95
- const payload = JSON.parse(content[0].text);
96
- expect(payload.success).toBe(true);
97
- expect(payload.context.activeWorkspaceId).toBe(
98
- "22222222-2222-2222-2222-222222222222",
99
- );
100
- expect(payload.context.activeProjectId).toBe(
101
- "11111111-1111-1111-1111-111111111111",
102
- );
103
- });
104
-
105
- test("callTool with unknown name returns isError content", async () => {
106
- const result = await client.callTool({
107
- name: "harmony_does_not_exist",
108
- arguments: {},
109
- });
110
- expect(result.isError).toBe(true);
111
- const content = result.content as Array<{ type: string; text: string }>;
112
- expect(content[0].text.toLowerCase()).toContain("error");
113
- });
114
-
115
- test("listResources delivers RESOURCES", async () => {
116
- const result = await client.listResources();
117
- expect(result.resources.length).toBe(RESOURCES.length);
118
- expect(result.resources[0].uri).toBe(RESOURCES[0].uri);
119
- });
120
-
121
- test("readResource serves harmony://context", async () => {
122
- const result = await client.readResource({ uri: "harmony://context" });
123
- const contents = result.contents as Array<{
124
- uri: string;
125
- mimeType?: string;
126
- text?: string;
127
- }>;
128
- expect(contents[0].uri).toBe("harmony://context");
129
- expect(contents[0].mimeType).toBe("application/json");
130
- const parsed = JSON.parse(contents[0].text ?? "{}");
131
- expect(parsed).toHaveProperty("configured");
132
- expect(parsed).toHaveProperty("activeWorkspaceId");
133
- expect(parsed).toHaveProperty("activeProjectId");
134
- });
135
-
136
- test("readResource on unknown URI surfaces JSON-RPC error", async () => {
137
- await expect(
138
- client.readResource({ uri: "harmony://unknown" }),
139
- ).rejects.toThrow(/Unknown resource/);
140
- });
141
- });
@@ -1,126 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { validateMemoryQuality } from "../memory-floor.js";
3
-
4
- const ok = (
5
- overrides: Partial<Parameters<typeof validateMemoryQuality>[0]> = {},
6
- ) => ({
7
- title: "AnnotatedTextarea auto-grow deps must include open state",
8
- content:
9
- "When the parent prop changes from closed to open, the auto-grow effect must re-measure. Without the dep, the textarea stays at the cached height and clips multi-line input.",
10
- type: "pattern",
11
- scope: "project",
12
- ...overrides,
13
- });
14
-
15
- describe("Memory Utility Floor (plan §4.5.1)", () => {
16
- test("accepts a real engineering lesson", () => {
17
- expect(validateMemoryQuality(ok())).toBeNull();
18
- });
19
-
20
- test("rejects comma-separated tag concatenation", () => {
21
- const r = validateMemoryQuality(
22
- ok({
23
- title: "Consolidated procedure: procedure, add, user",
24
- content:
25
- "Consolidated from 5 procedure memories: foo bar baz qux many words.",
26
- }),
27
- );
28
- expect(r?.rule).toBe("tag-concat");
29
- });
30
-
31
- test("rejects slash-separated tag concatenation", () => {
32
- const r = validateMemoryQuality(
33
- ok({
34
- title: "Procedure: Procedure / Card / Mobile / Native",
35
- content:
36
- "3 related procedure entities consolidated. Original titles list goes here.",
37
- }),
38
- );
39
- expect(r?.rule).toBe("tag-concat-slash");
40
- });
41
-
42
- test("rejects frequency-meta titles", () => {
43
- const r = validateMemoryQuality(
44
- ok({
45
- title: "Pattern: recurring procedure (17 instances)",
46
- content: "Recurring pattern: procedure entities appearing 19 times.",
47
- }),
48
- );
49
- expect(r?.rule).toBe("frequency-meta");
50
- });
51
-
52
- test("rejects bare type prefix with empty content after colon", () => {
53
- const r = validateMemoryQuality(
54
- ok({
55
- title: "Memory:",
56
- content:
57
- "Not enough substance here for any kind of memory entry value.",
58
- }),
59
- );
60
- expect(r?.rule).toBe("bare-type-prefix");
61
- });
62
-
63
- test("rejects self-referential operational notes", () => {
64
- const r = validateMemoryQuality(
65
- ok({
66
- title: "Auto-consolidation produces tautological clusters on tags",
67
- content:
68
- "The consolidation pipeline clusters entities by tag overlap, but doesn't distinguish content-bearing tags from structural pipeline tags.",
69
- type: "lesson",
70
- }),
71
- );
72
- expect(r?.rule).toBe("self-referential");
73
- });
74
-
75
- test("rejects content that's too short", () => {
76
- const r = validateMemoryQuality(
77
- ok({
78
- title: "AnnotatedTextarea auto-grow deps fix",
79
- content: "Short.",
80
- }),
81
- );
82
- expect(r?.rule).toBe("length-floor");
83
- });
84
-
85
- test("rejects under-specific titles", () => {
86
- const r = validateMemoryQuality(
87
- ok({
88
- title: "fix the bug",
89
- content:
90
- "There is a bug somewhere and it should be fixed at some point in the future.",
91
- }),
92
- );
93
- expect(r?.rule).toBe("specificity-floor");
94
- });
95
-
96
- test("rejects Agent Profile dumps", () => {
97
- const r = validateMemoryQuality({
98
- title: "Agent Profile: claude-code",
99
- content:
100
- "## claude-code Performance Profile - Total sessions: 183 - Completed: 180 (98%)",
101
- type: "agent",
102
- });
103
- expect(r?.rule).toBe("operational-data-ban");
104
- });
105
-
106
- test("session-scope memories bypass the Floor entirely", () => {
107
- const r = validateMemoryQuality({
108
- title: "user wants Q3 revenue rolled up by team",
109
- content: "x",
110
- type: "context",
111
- scope: "session:abc123",
112
- });
113
- expect(r).toBeNull();
114
- });
115
-
116
- test("doc-source memories bypass title checks but not length", () => {
117
- const r = validateMemoryQuality({
118
- title: "## API Routes",
119
- content:
120
- "Section heading from CLAUDE.md auto-import. Long enough body content here ok.",
121
- type: "context",
122
- source_trust: "document",
123
- });
124
- expect(r).toBeNull();
125
- });
126
- });