Skip to content

Commit d8d57b8

Browse files
committed
fix(memory): include type field in output schemas to fix validation error
The memory server stores entities with a 'type' field in JSONL, but the tool output schemas did not include this property. When structured output validation is enabled, this causes MCP error -32602 for read_graph, search_nodes, and open_nodes tools. Fixes #3074
1 parent 4503e2d commit d8d57b8

2 files changed

Lines changed: 16 additions & 13 deletions

File tree

src/memory/__tests__/knowledge-graph.test.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ describe('KnowledgeGraphManager', () => {
426426
expect(JSON.parse(lines[1])).toHaveProperty('type', 'relation');
427427
});
428428

429-
it('should strip type field from entities when loading from file', async () => {
429+
it('should preserve JSONL type discriminator when loading from file', async () => {
430430
// Create entities and relations (these get saved with type field)
431431
await manager.createEntities([
432432
{ name: 'Alice', entityType: 'person', observations: ['test observation'] },
@@ -451,26 +451,25 @@ describe('KnowledgeGraphManager', () => {
451451
const manager2 = new KnowledgeGraphManager(testFilePath);
452452
const graph = await manager2.readGraph();
453453

454-
// Verify loaded entities don't have type field
454+
// Loaded graph mirrors JSONL discriminators for structured-output validation (#3074)
455455
expect(graph.entities).toHaveLength(2);
456456
graph.entities.forEach(entity => {
457-
expect(entity).not.toHaveProperty('type');
457+
expect(entity).toHaveProperty('type', 'entity');
458458
expect(entity).toHaveProperty('name');
459459
expect(entity).toHaveProperty('entityType');
460460
expect(entity).toHaveProperty('observations');
461461
});
462462

463-
// Verify loaded relations don't have type field
464463
expect(graph.relations).toHaveLength(1);
465464
graph.relations.forEach(relation => {
466-
expect(relation).not.toHaveProperty('type');
465+
expect(relation).toHaveProperty('type', 'relation');
467466
expect(relation).toHaveProperty('from');
468467
expect(relation).toHaveProperty('to');
469468
expect(relation).toHaveProperty('relationType');
470469
});
471470
});
472471

473-
it('should strip type field from searchNodes results', async () => {
472+
it('should include type discriminator in searchNodes results after reload', async () => {
474473
await manager.createEntities([
475474
{ name: 'Alice', entityType: 'person', observations: ['works at Acme'] },
476475
]);
@@ -482,17 +481,16 @@ describe('KnowledgeGraphManager', () => {
482481
const manager2 = new KnowledgeGraphManager(testFilePath);
483482
const result = await manager2.searchNodes('Alice');
484483

485-
// Verify search results don't have type field
486484
expect(result.entities).toHaveLength(1);
487-
expect(result.entities[0]).not.toHaveProperty('type');
485+
expect(result.entities[0]).toHaveProperty('type', 'entity');
488486
expect(result.entities[0].name).toBe('Alice');
489487

490488
expect(result.relations).toHaveLength(1);
491-
expect(result.relations[0]).not.toHaveProperty('type');
489+
expect(result.relations[0]).toHaveProperty('type', 'relation');
492490
expect(result.relations[0].from).toBe('Alice');
493491
});
494492

495-
it('should strip type field from openNodes results', async () => {
493+
it('should include type discriminator in openNodes results after reload', async () => {
496494
await manager.createEntities([
497495
{ name: 'Alice', entityType: 'person', observations: [] },
498496
{ name: 'Bob', entityType: 'person', observations: [] },
@@ -505,14 +503,13 @@ describe('KnowledgeGraphManager', () => {
505503
const manager2 = new KnowledgeGraphManager(testFilePath);
506504
const result = await manager2.openNodes(['Alice', 'Bob']);
507505

508-
// Verify open results don't have type field
509506
expect(result.entities).toHaveLength(2);
510507
result.entities.forEach(entity => {
511-
expect(entity).not.toHaveProperty('type');
508+
expect(entity).toHaveProperty('type', 'entity');
512509
});
513510

514511
expect(result.relations).toHaveLength(1);
515-
expect(result.relations[0]).not.toHaveProperty('type');
512+
expect(result.relations[0]).toHaveProperty('type', 'relation');
516513
});
517514
});
518515
});

src/memory/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ let MEMORY_FILE_PATH: string;
4848

4949
// We are storing our memory using entities, relations, and observations in a graph structure
5050
export interface Entity {
51+
type?: "entity";
5152
name: string;
5253
entityType: string;
5354
observations: string[];
5455
}
5556

5657
export interface Relation {
58+
type?: "relation";
5759
from: string;
5860
to: string;
5961
relationType: string;
@@ -76,13 +78,15 @@ export class KnowledgeGraphManager {
7678
const item = JSON.parse(line);
7779
if (item.type === "entity") {
7880
graph.entities.push({
81+
type: "entity",
7982
name: item.name,
8083
entityType: item.entityType,
8184
observations: item.observations
8285
});
8386
}
8487
if (item.type === "relation") {
8588
graph.relations.push({
89+
type: "relation",
8690
from: item.from,
8791
to: item.to,
8892
relationType: item.relationType
@@ -241,12 +245,14 @@ let knowledgeGraphManager: KnowledgeGraphManager;
241245

242246
// Zod schemas for entities and relations
243247
const EntitySchema = z.object({
248+
type: z.literal("entity").optional().describe('Discriminator when parsed from JSONL; omit when creating entities'),
244249
name: z.string().describe("The name of the entity"),
245250
entityType: z.string().describe("The type of the entity"),
246251
observations: z.array(z.string()).describe("An array of observation contents associated with the entity")
247252
});
248253

249254
const RelationSchema = z.object({
255+
type: z.literal("relation").optional().describe('Discriminator when parsed from JSONL; omit when creating relations'),
250256
from: z.string().describe("The name of the entity where the relation starts"),
251257
to: z.string().describe("The name of the entity where the relation ends"),
252258
relationType: z.string().describe("The type of the relation")

0 commit comments

Comments
 (0)