rag-memory-epf-mcp 3.5.2 → 4.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.
@@ -563,7 +563,8 @@ Supports both merge (additive) and replace (clear + import) modes.
563
563
  - Replace mode provides clean import without conflicts
564
564
  - Handles partial imports (entities only, relations only, etc.)
565
565
  - Reports imported and skipped counts for verification
566
- - Supports full export format from exportGraph
566
+ - Supports the full exportGraph format, including the v13 observation lifecycle tables
567
+ (revision history, provenance and events survive a round-trip)
567
568
  </features>
568
569
 
569
570
  <bestPractices>
@@ -587,12 +588,35 @@ Supports both merge (additive) and replace (clear + import) modes.
587
588
  - Full replace: {"data": {"entities": [...], "relations": [...], "documents": [...]}, "merge": false}
588
589
  - Entities only: {"data": {"entities": [{"id": "abc", "name": "Test", "entityType": "CONCEPT", "observations": ["fact1"]}]}}
589
590
  </examples>`;
591
+ // dump 의 행은 객체다. z.any() 로 두면 광고 스키마에 type:'string' fallback 으로 나가
592
+ // listTools 계약이 실제 입력과 어긋난다. z.record 는 "문자열 키를 가진 객체"로 광고되고
593
+ // 비객체 입력을 거부한다(advisor beta r3 남은 P2).
594
+ const dumpRow = () => z.record(z.any());
590
595
  const importGraphSchema = {
591
596
  data: z.object({
592
- entities: z.array(z.any()).optional().describe('Array of entity objects to import'),
593
- relations: z.array(z.any()).optional().describe('Array of relation objects to import'),
594
- documents: z.array(z.any()).optional().describe('Array of document objects to import'),
595
- }).describe('Object containing entities, relations, and/or documents arrays to import'),
597
+ entities: z.array(dumpRow()).optional().describe('Array of entity objects to import'),
598
+ relations: z.array(dumpRow()).optional().describe('Array of relation objects to import'),
599
+ documents: z.array(dumpRow()).optional().describe('Array of document objects to import'),
600
+ // v13: 이 네 배열이 스키마에 없으면 z.object().parse() 가 조용히 버린다. 그러면
601
+ // exportGraph 가 낸 완전한 dump 를 MCP 로 되돌릴 때 revision history·provenance·
602
+ // event log 가 전부 사라지고 legacy 관찰로 재생성된다 — 백업/복원이 조용히
603
+ // 손실 연산이 된다(advisor beta 발견 1, MCP 왕복으로 실측).
604
+ observation_roots: z.array(dumpRow()).optional()
605
+ .describe('Array of observation root rows (v13 lifecycle). Required to preserve history on restore'),
606
+ entity_observations: z.array(dumpRow()).optional()
607
+ .describe('Array of observation revision rows (v13 lifecycle)'),
608
+ observation_sources: z.array(dumpRow()).optional()
609
+ .describe('Array of observation provenance rows (v13 lifecycle)'),
610
+ observation_events: z.array(dumpRow()).optional()
611
+ .describe('Array of observation event rows (v13 lifecycle)'),
612
+ metadata: z.record(z.any()).optional()
613
+ .describe('Export metadata (exportedAt, version, counts). Ignored on import'),
614
+ // strict: dump 의 모든 정상 키를 위에 열거했으므로, 모르는 키는 **거부**한다.
615
+ // 기본 strip 은 모르는 테이블을 조용히 버리고(그게 lifecycle 4배열에서 실제로
616
+ // 일어난 일이다), passthrough 는 그것을 manager 까지 흘려보내 거기서 다시 조용히
617
+ // 무시된다 — 둘 다 같은 미래 데이터 손실 경로다. 지원하지 않는 dump 는 크게 실패해야
618
+ // 운영자가 엔진을 올린다(advisor beta r3).
619
+ }).strict().describe('A graph dump as produced by exportGraph, including the v13 observation lifecycle tables'),
596
620
  merge: z.boolean().optional().default(true).describe('If true (default), merge with existing data. If false, clear existing data first.'),
597
621
  };
598
622
  export const importGraphTool = {
@@ -6,6 +6,13 @@ export declare const hybridSearchTool: ToolDefinition;
6
6
  export declare const embedAllEntitiesTool: ToolDefinition;
7
7
  export declare const getDetailedContextTool: ToolDefinition;
8
8
  export declare const updateRelationsTool: ToolDefinition;
9
+ export declare const retractObservationTool: ToolDefinition;
10
+ export declare const restoreObservationTool: ToolDefinition;
11
+ export declare const approveObservationTool: ToolDefinition;
12
+ export declare const declineObservationTool: ToolDefinition;
13
+ export declare const correctObservationTool: ToolDefinition;
14
+ export declare const purgeObservationTool: ToolDefinition;
15
+ export declare const getObservationHistoryTool: ToolDefinition;
9
16
  export declare const knowledgeGraphTools: {
10
17
  createEntities: ToolDefinition;
11
18
  createRelations: ToolDefinition;
@@ -14,4 +21,11 @@ export declare const knowledgeGraphTools: {
14
21
  hybridSearch: ToolDefinition;
15
22
  embedAllEntities: ToolDefinition;
16
23
  getDetailedContext: ToolDefinition;
24
+ correctObservation: ToolDefinition;
25
+ retractObservation: ToolDefinition;
26
+ restoreObservation: ToolDefinition;
27
+ approveObservation: ToolDefinition;
28
+ declineObservation: ToolDefinition;
29
+ purgeObservation: ToolDefinition;
30
+ getObservationHistory: ToolDefinition;
17
31
  };
@@ -73,6 +73,15 @@ const createEntitiesSchema = {
73
73
  name: z.string().describe('The unique name/identifier of the entity'),
74
74
  entityType: z.string().describe('The category or type of the entity (e.g., PERSON, CONCEPT, TECHNOLOGY)'),
75
75
  observations: z.array(z.string()).describe('Array of contextual observations about the entity'),
76
+ // v13: 아래 두 필드는 addObservations 와 같은 의미다. 스키마에 없으면 전달되지 않는다.
77
+ status: z.enum(['active', 'provisional']).optional()
78
+ .describe("'provisional' hides the observations from search until approveObservation. Default 'active'"),
79
+ sources: z.array(z.object({
80
+ source_kind: z.enum(['document', 'conversation', 'decision', 'import'])
81
+ .describe('What kind of thing these observations came from'),
82
+ source_ref: z.string().describe('Identifier of the source'),
83
+ source_hash: z.string().optional().describe('Optional content hash of the source'),
84
+ })).optional().describe('Provenance. Omit if genuinely unknown - do not invent a source'),
76
85
  })).describe('Array of entities to create in the knowledge graph'),
77
86
  };
78
87
  export const createEntitiesTool = {
@@ -189,8 +198,10 @@ Observations provide the factual foundation that supports entity existence and p
189
198
 
190
199
  <importantNotes>
191
200
  - (!important!) **Entity must exist** - this tool only adds to existing entities
192
- - (!important!) Only new observations are added - duplicates are automatically filtered
193
- - (!important!) Observations are cumulative - they build the entity's knowledge base
201
+ - (!important!) Duplicates are filtered - repeating existing text with a **new** sources entry
202
+ adds evidence to the existing revision and returns null for that position
203
+ - (!important!) Observations are cumulative. **This tool never replaces anything** - use
204
+ correctObservation to supersede and retractObservation to withdraw
194
205
  - (!important!) **Be specific and factual** - observations should be verifiable statements
195
206
  </importantNotes>
196
207
 
@@ -200,21 +211,24 @@ Observations provide the factual foundation that supports entity existence and p
200
211
  - When updating entity knowledge from new sources
201
212
  - When refining and expanding entity descriptions
202
213
  - **Before making knowledge-based decisions** - ensure entities have sufficient context
203
- - When correcting or expanding incomplete entity information
214
+ - When *expanding* incomplete entity information. **To correct a wrong observation use
215
+ correctObservation** - adding a second, contradicting observation leaves both in search
204
216
  </whenToUseThisTool>
205
217
 
206
218
  <features>
207
219
  - Batch addition of observations to multiple entities
208
220
  - Automatic duplicate filtering - no redundant observations
209
221
  - Supports rich textual observations with context
210
- - Maintains observation history and chronology
222
+ - Stable observation ids returned in observation_ids, aligned 1:1 with contents
223
+ - Real revision history: see correctObservation and getObservationHistory
211
224
  - Integrates with document processing workflows
212
225
  - Enables incremental knowledge building
213
226
  </features>
214
227
 
215
228
  <bestPractices>
216
229
  - Keep observations factual and specific rather than general
217
- - Include source context when possible ("According to paper X...")
230
+ - Put provenance in the sources field, not in the text. A source written in prose cannot be
231
+ queried, deduplicated, or attached to a later revision
218
232
  - Use consistent terminology across observations
219
233
  - Add complementary observations that provide different perspectives
220
234
  - Include temporal information when relevant ("As of 2024...")
@@ -232,10 +246,22 @@ Observations provide the factual foundation that supports entity existence and p
232
246
  - Person details: {"observations": [{"entityName": "Marie Curie", "contents": ["First woman to win Nobel Prize", "Won Nobel Prizes in two different sciences"]}]}
233
247
  - Technology evolution: {"observations": [{"entityName": "React", "contents": ["React 18 introduced concurrent features", "Widely adopted for enterprise applications"]}]}
234
248
  </examples>`;
249
+ // v13: provenance 와 status 는 스키마에 있어야 실제로 전달된다. validateToolArgs 가
250
+ // z.object(...).parse() 로 검증하므로 스키마에 없는 키는 조용히 버려진다.
251
+ const sourceInputSchema = z.object({
252
+ source_kind: z.enum(['document', 'conversation', 'decision', 'import'])
253
+ .describe('What kind of thing this observation came from'),
254
+ source_ref: z.string().describe('Identifier of the source (document id, decision id, ...)'),
255
+ source_hash: z.string().optional().describe('Optional content hash of the source'),
256
+ }).describe('Where this observation came from');
235
257
  const addObservationsSchema = {
236
258
  observations: z.array(z.object({
237
259
  entityName: z.string().describe('Name of the existing entity to add observations to'),
238
260
  contents: z.array(z.string()).describe('Array of new observation strings to add'),
261
+ status: z.enum(['active', 'provisional']).optional()
262
+ .describe("'provisional' hides it from search until approveObservation. Default 'active'"),
263
+ sources: z.array(sourceInputSchema).optional()
264
+ .describe('Provenance. Omit if genuinely unknown - do not invent a source'),
239
265
  })).describe('Array of observation additions for specific entities'),
240
266
  };
241
267
  export const addObservationsTool = {
@@ -538,6 +564,211 @@ export const updateRelationsTool = {
538
564
  schema: updateRelationsSchema,
539
565
  annotations: { idempotentHint: true },
540
566
  };
567
+ // === OBSERVATION LIFECYCLE TOOLS (v13, spec §6.1 / §6.2) ===
568
+ // 상태 전이 4종은 인자와 형태가 같다. 같은 문장을 네 번 복사하면 한 곳만 고치는
569
+ // drift 가 생기므로 팩토리로 만든다.
570
+ const transitionTool = (a) => ({
571
+ capability: {
572
+ description: `${a.verb} an observation revision (${a.from} -> ${a.to})`,
573
+ parameters: {
574
+ type: 'object',
575
+ properties: {
576
+ observation_id: { type: 'string', description: 'The revision to transition' },
577
+ reason: { type: 'string', description: 'Why this transition happened' },
578
+ },
579
+ required: a.reasonRequired ? ['observation_id', 'reason'] : ['observation_id'],
580
+ },
581
+ },
582
+ description: () => `<description>
583
+ ${a.verb} an observation revision: status '${a.from}' becomes '${a.to}'.
584
+ **This is a state change, not a deletion** - the revision and its text survive and stay
585
+ visible through getObservationHistory.
586
+ </description>
587
+
588
+ <importantNotes>
589
+ - (!important!) Only '${a.from}' revisions can be ${a.to === 'active' ? 'moved to active' : a.to}; the transition table rejects anything else
590
+ - (!important!) 'superseded' is terminal - correct it into a new revision instead
591
+ - (!important!) Ordinary search returns only 'active' revisions, so this changes what search sees
592
+ - (!important!) ${a.extra}
593
+ </importantNotes>
594
+
595
+ <whenToUseThisTool>
596
+ - ${a.extra}
597
+ - When you need the record of the change to survive, not just the current text
598
+ </whenToUseThisTool>
599
+
600
+ <parameters>
601
+ - observation_id: The revision id, from addObservations/createEntities or getObservationHistory (string, required)
602
+ - reason: Why (string, ${a.reasonRequired ? 'required' : 'optional but strongly recommended'})
603
+ </parameters>`,
604
+ schema: {
605
+ observation_id: z.string().describe('The observation revision id to transition'),
606
+ ...(a.reasonRequired
607
+ ? { reason: z.string().describe('Why this transition happened') }
608
+ : { reason: z.string().optional().describe('Why this transition happened') }),
609
+ },
610
+ });
611
+ export const retractObservationTool = transitionTool({
612
+ verb: 'Retract', from: 'active', to: 'retracted', reasonRequired: false,
613
+ extra: 'Use this when a fact turned out to be wrong or no longer holds and there is no replacement text',
614
+ });
615
+ export const restoreObservationTool = transitionTool({
616
+ verb: 'Restore', from: 'retracted', to: 'active', reasonRequired: false,
617
+ extra: 'Use this when a retraction itself was a mistake',
618
+ });
619
+ export const approveObservationTool = transitionTool({
620
+ verb: 'Approve', from: 'provisional', to: 'active', reasonRequired: false,
621
+ extra: 'Use this to accept a provisional observation so search starts returning it',
622
+ });
623
+ export const declineObservationTool = transitionTool({
624
+ verb: 'Decline', from: 'provisional', to: 'retracted', reasonRequired: true,
625
+ extra: 'Use this to reject a provisional observation while keeping the fact that it was proposed',
626
+ });
627
+ const correctObservationDescription = () => `<description>
628
+ Replace the text of an observation while keeping the previous version as history.
629
+ **This is the tool to use when a stored fact is wrong.** Overwriting loses the correction;
630
+ this supersedes the old revision so both the mistake and the fix stay retrievable.
631
+ </description>
632
+
633
+ <importantNotes>
634
+ - (!important!) The old revision becomes 'superseded' and the new one becomes 'active'
635
+ - (!important!) The new revision keeps the old one's position in the entity's observation array
636
+ - (!important!) Only an 'active' revision can be corrected
637
+ - (!important!) Ordinary search returns only the new text; the old text is reachable through getObservationHistory
638
+ </importantNotes>
639
+
640
+ <whenToUseThisTool>
641
+ - When an observation states something that is now known to be false (change_kind: 'correction')
642
+ - When the world changed and the old statement was true at the time (change_kind: 'world_change')
643
+ - **Instead of deleting and re-adding** - that loses the link between the two
644
+ </whenToUseThisTool>
645
+
646
+ <parameters>
647
+ - observation_id: The active revision to correct (string, required)
648
+ - content: The corrected text (string, required)
649
+ - change_kind: 'correction' (it was wrong) or 'world_change' (it stopped being true). Default 'correction'
650
+ - reason: Why (string, optional but strongly recommended)
651
+ </parameters>
652
+
653
+ <examples>
654
+ - {"observation_id": "…", "content": "Uses FTS5 for keyword search", "change_kind": "correction", "reason": "verified in source"}
655
+ </examples>`;
656
+ export const correctObservationTool = {
657
+ capability: {
658
+ description: 'Supersede an observation revision with corrected text, keeping the old one as history',
659
+ parameters: {
660
+ type: 'object',
661
+ properties: {
662
+ observation_id: { type: 'string', description: 'The active revision to correct' },
663
+ content: { type: 'string', description: 'The corrected text' },
664
+ change_kind: { type: 'string', description: "'correction' or 'world_change'" },
665
+ reason: { type: 'string', description: 'Why this correction happened' },
666
+ },
667
+ required: ['observation_id', 'content'],
668
+ },
669
+ },
670
+ description: correctObservationDescription,
671
+ schema: {
672
+ observation_id: z.string().describe('The active observation revision to correct'),
673
+ content: z.string().describe('The corrected observation text'),
674
+ change_kind: z.enum(['correction', 'world_change']).optional()
675
+ .describe("'correction' = it was wrong; 'world_change' = it stopped being true. Default 'correction'"),
676
+ reason: z.string().optional().describe('Why this correction happened'),
677
+ },
678
+ };
679
+ const purgeObservationDescription = () => `<description>
680
+ **DESTRUCTIVE.** Physically delete an observation revision and every later revision of the same
681
+ root. History is gone afterwards - there is no undo.
682
+ </description>
683
+
684
+ <importantNotes>
685
+ - (!important!) **retractObservation is almost always what you want.** It hides the fact from
686
+ search while keeping the record
687
+ - (!important!) You must pass confirm='PURGE'; any other value refuses
688
+ - (!important!) This is a **suffix purge**: purging revision 2 of a 3-revision chain removes 3 then 2
689
+ - (!important!) The root row is kept so the observation's array position stays reserved
690
+ - (!important!) Events for purged revisions are removed too
691
+ </importantNotes>
692
+
693
+ <whenToUseThisTool>
694
+ - When content must be erased for legal or privacy reasons, not because it is wrong
695
+ - **Never** as the normal way to remove an observation
696
+ </whenToUseThisTool>
697
+
698
+ <parameters>
699
+ - observation_id: The revision to purge from (string, required)
700
+ - confirm: Must be exactly 'PURGE' (string, required)
701
+ </parameters>`;
702
+ export const purgeObservationTool = {
703
+ capability: {
704
+ description: 'DESTRUCTIVE: physically delete an observation revision and its successors',
705
+ parameters: {
706
+ type: 'object',
707
+ properties: {
708
+ observation_id: { type: 'string', description: 'The revision to purge from' },
709
+ confirm: { type: 'string', description: "Must be exactly 'PURGE'" },
710
+ },
711
+ required: ['observation_id', 'confirm'],
712
+ },
713
+ },
714
+ description: purgeObservationDescription,
715
+ schema: {
716
+ observation_id: z.string().describe('The observation revision to purge from'),
717
+ confirm: z.literal('PURGE').describe("Must be exactly 'PURGE' - this destroys history"),
718
+ },
719
+ annotations: { destructiveHint: true },
720
+ };
721
+ const getObservationHistoryDescription = () => `<description>
722
+ Return the full revision history of observations: every version, its status, its provenance, and
723
+ the events that changed it.
724
+ **Past versions come out here and nowhere else - ordinary search returns only 'active' revisions.**
725
+ </description>
726
+
727
+ <importantNotes>
728
+ - (!important!) The response is always \`{ roots: [...] }\` - one root per logical observation
729
+ - (!important!) Each root holds its revisions in order (revision_no ascending), oldest first
730
+ - (!important!) Use this to answer "was this ever different?" or "where did this come from?"
731
+ - (!important!) Exactly one selector is required
732
+ </importantNotes>
733
+
734
+ <whenToUseThisTool>
735
+ - When you need to know whether a fact was corrected, and what it used to say
736
+ - When you need the provenance (which document or decision) behind an observation
737
+ - Before trusting a surprising fact - check whether it was already retracted or superseded
738
+ - When auditing what changed in an entity's knowledge over time
739
+ </whenToUseThisTool>
740
+
741
+ <parameters>
742
+ - entity_name: All observations of this entity (string, optional)
743
+ - observation_id: The root that this revision belongs to (string, optional)
744
+ - root_id: One specific logical observation (string, optional)
745
+ </parameters>
746
+
747
+ <examples>
748
+ - {"entity_name": "claude-mem"}
749
+ - {"observation_id": "…"}
750
+ </examples>`;
751
+ export const getObservationHistoryTool = {
752
+ capability: {
753
+ description: 'Full revision history of observations - the only surface for non-active revisions',
754
+ parameters: {
755
+ type: 'object',
756
+ properties: {
757
+ entity_name: { type: 'string', description: 'All observations of this entity' },
758
+ observation_id: { type: 'string', description: 'The root this revision belongs to' },
759
+ root_id: { type: 'string', description: 'One specific logical observation' },
760
+ },
761
+ required: [],
762
+ },
763
+ },
764
+ description: getObservationHistoryDescription,
765
+ schema: {
766
+ entity_name: z.string().optional().describe('All observations of this entity'),
767
+ observation_id: z.string().optional().describe('The root that this revision belongs to'),
768
+ root_id: z.string().optional().describe('One specific logical observation'),
769
+ },
770
+ annotations: { readOnlyHint: true },
771
+ };
541
772
  // Export all knowledge graph tools
542
773
  export const knowledgeGraphTools = {
543
774
  createEntities: createEntitiesTool,
@@ -547,4 +778,12 @@ export const knowledgeGraphTools = {
547
778
  hybridSearch: hybridSearchTool,
548
779
  embedAllEntities: embedAllEntitiesTool,
549
780
  getDetailedContext: getDetailedContextTool,
781
+ // v13 observation lifecycle
782
+ correctObservation: correctObservationTool,
783
+ retractObservation: retractObservationTool,
784
+ restoreObservation: restoreObservationTool,
785
+ approveObservation: approveObservationTool,
786
+ declineObservation: declineObservationTool,
787
+ purgeObservation: purgeObservationTool,
788
+ getObservationHistory: getObservationHistoryTool,
550
789
  };
@@ -28,6 +28,13 @@ export declare const allTools: {
28
28
  hybridSearch: ToolDefinition;
29
29
  embedAllEntities: ToolDefinition;
30
30
  getDetailedContext: ToolDefinition;
31
+ correctObservation: ToolDefinition;
32
+ retractObservation: ToolDefinition;
33
+ restoreObservation: ToolDefinition;
34
+ approveObservation: ToolDefinition;
35
+ declineObservation: ToolDefinition;
36
+ purgeObservation: ToolDefinition;
37
+ getObservationHistory: ToolDefinition;
31
38
  };
32
39
  export declare const globalSettings: {
33
40
  version: string;
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { knowledgeGraphTools } from './knowledge-graph-tools.js';
3
4
  import { ragTools } from './rag-tools.js';
4
5
  import { graphQueryTools } from './graph-query-tools.js';
@@ -12,9 +13,12 @@ export const allTools = {
12
13
  ...graphAnalyticsTools,
13
14
  ...migrationTools,
14
15
  };
15
- // Global settings for tool descriptions
16
+ // Global settings for tool descriptions.
17
+ // version 은 package.json 이 정본이다. 하드코딩하면 릴리스마다 이 값만 옛 버전으로
18
+ // 남아 도구 문서가 거짓을 말한다(실제로 3.1.0 에 멈춰 있었다 — advisor beta r3 남은 P2).
19
+ const PKG = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'));
16
20
  export const globalSettings = {
17
- version: '3.1.0',
21
+ version: PKG.version,
18
22
  systemName: 'RAG Knowledge Graph MCP Server',
19
23
  defaultTimeout: 60,
20
24
  };
@@ -41,6 +45,10 @@ export function convertToMCPTool(name, toolDef) {
41
45
  type: 'object',
42
46
  properties,
43
47
  required,
48
+ // validateToolArgs 가 최상위를 strict 로 검증한다. 광고 스키마가 그걸 말하지
49
+ // 않으면 클라이언트는 여분 필드가 허용된다고 읽고 서버에서 거부당한다
50
+ // (advisor beta r3: 계약 표현 불일치).
51
+ additionalProperties: false,
44
52
  },
45
53
  ...(toolDef.annotations && { annotations: toolDef.annotations }),
46
54
  };
@@ -90,6 +98,10 @@ function zodTypeToJsonSchema(zodType, fieldName) {
90
98
  description: def.description || `${fieldName} object`,
91
99
  properties: objectProperties,
92
100
  required: objectRequired,
101
+ // 런타임이 strict 면 광고도 strict 라고 말해야 한다. 안 그러면 클라이언트가
102
+ // 여분 키를 허용된다고 읽고 서버에서 거부당한다 — importGraph.data 가
103
+ // 정확히 그 상태였다(advisor beta r4 남은 P2).
104
+ ...(def.unknownKeys === 'strict' && { additionalProperties: false }),
93
105
  };
94
106
  case 'ZodRecord':
95
107
  return {
@@ -97,6 +109,30 @@ function zodTypeToJsonSchema(zodType, fieldName) {
97
109
  description: def.description || `${fieldName} record`,
98
110
  additionalProperties: true,
99
111
  };
112
+ // Enums and literals used to fall through to the string fallback, which dropped the
113
+ // allowed values from the advertised schema even though parse() still enforced them.
114
+ // A client that cannot see the values guesses, and the guess is rejected server-side.
115
+ case 'ZodEnum':
116
+ return {
117
+ type: 'string',
118
+ description: def.description || `${fieldName} parameter`,
119
+ enum: [...def.values],
120
+ };
121
+ case 'ZodLiteral':
122
+ return {
123
+ type: typeof def.value === 'number' ? 'number'
124
+ : typeof def.value === 'boolean' ? 'boolean' : 'string',
125
+ description: def.description || `${fieldName} parameter`,
126
+ enum: [def.value],
127
+ };
128
+ // union 도 같은 fallback 함정이었다: deleteDocuments 의 documentIds 는
129
+ // string | string[] 인데 광고 스키마에 type:'string' 으로 나가 배열을 보내면
130
+ // 클라이언트가 계약 위반이라고 읽는다. anyOf 로 정직하게 노출한다.
131
+ case 'ZodUnion':
132
+ return {
133
+ description: def.description || `${fieldName} parameter`,
134
+ anyOf: def.options.map((o, i) => zodTypeToJsonSchema(o, `${fieldName} option ${i}`)),
135
+ };
100
136
  case 'ZodOptional':
101
137
  const innerSchema = zodTypeToJsonSchema(def.innerType, fieldName);
102
138
  return {
@@ -143,7 +179,11 @@ export function validateToolArgs(toolName, args) {
143
179
  if (!toolDef) {
144
180
  throw new Error(`Unknown tool: ${toolName}`);
145
181
  }
146
- const schema = z.object(toolDef.schema);
182
+ // strict: 알 수 없는 최상위 인자는 거부한다. 기본 strip 은 오래된 호출자를
183
+ // **조용히** 통과시킨다 — v3.6 의 index 기반 관찰 지정(`{observation_id, index}`,
184
+ // `{observation_index}`)이 아무 오류 없이 무시되고, 호출자는 자기가 지정한
185
+ // revision 이 아닌 다른 것이 처리됐다는 사실을 모른다(spec T6, advisor beta 발견 4-1).
186
+ const schema = z.object(toolDef.schema).strict();
147
187
  return schema.parse(args);
148
188
  }
149
189
  /**
@@ -36,6 +36,7 @@ export interface MCPTool {
36
36
  type: 'object';
37
37
  properties: Record<string, any>;
38
38
  required: string[];
39
+ additionalProperties?: boolean;
39
40
  };
40
41
  annotations?: ToolAnnotations;
41
42
  }