@unclick/mcp-server 0.3.16 → 0.3.18

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 (43) hide show
  1. package/dist/__tests__/legalpass-tool.test.d.ts +2 -0
  2. package/dist/__tests__/legalpass-tool.test.d.ts.map +1 -0
  3. package/dist/__tests__/legalpass-tool.test.js +44 -0
  4. package/dist/__tests__/legalpass-tool.test.js.map +1 -0
  5. package/dist/__tests__/tool-schema-validation.test.d.ts +2 -0
  6. package/dist/__tests__/tool-schema-validation.test.d.ts.map +1 -0
  7. package/dist/__tests__/tool-schema-validation.test.js +54 -0
  8. package/dist/__tests__/tool-schema-validation.test.js.map +1 -0
  9. package/dist/copypass-tool.d.ts +3 -0
  10. package/dist/copypass-tool.d.ts.map +1 -0
  11. package/dist/copypass-tool.js +123 -0
  12. package/dist/copypass-tool.js.map +1 -0
  13. package/dist/copypass-tool.test.d.ts +2 -0
  14. package/dist/copypass-tool.test.d.ts.map +1 -0
  15. package/dist/copypass-tool.test.js +41 -0
  16. package/dist/copypass-tool.test.js.map +1 -0
  17. package/dist/legalpass-tool.d.ts +15 -0
  18. package/dist/legalpass-tool.d.ts.map +1 -0
  19. package/dist/legalpass-tool.js +121 -0
  20. package/dist/legalpass-tool.js.map +1 -0
  21. package/dist/memory/__tests__/response-bounds.test.js +63 -1
  22. package/dist/memory/__tests__/response-bounds.test.js.map +1 -1
  23. package/dist/memory/handlers.d.ts +1 -0
  24. package/dist/memory/handlers.d.ts.map +1 -1
  25. package/dist/memory/handlers.js +91 -3
  26. package/dist/memory/handlers.js.map +1 -1
  27. package/dist/memory/local.d.ts.map +1 -1
  28. package/dist/memory/local.js +2 -0
  29. package/dist/memory/local.js.map +1 -1
  30. package/dist/memory/supabase.d.ts.map +1 -1
  31. package/dist/memory/supabase.js +2 -0
  32. package/dist/memory/supabase.js.map +1 -1
  33. package/dist/memory/types.d.ts +2 -0
  34. package/dist/memory/types.d.ts.map +1 -1
  35. package/dist/server.d.ts +12 -0
  36. package/dist/server.d.ts.map +1 -1
  37. package/dist/server.js +141 -3
  38. package/dist/server.js.map +1 -1
  39. package/dist/tool-wiring.d.ts +944 -0
  40. package/dist/tool-wiring.d.ts.map +1 -1
  41. package/dist/tool-wiring.js +903 -56
  42. package/dist/tool-wiring.js.map +1 -1
  43. package/package.json +2 -1
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import * as AjvModule from "ajv";
3
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
5
  import { CATALOG, TOOL_MAP, ENDPOINT_MAP } from "./catalog.js";
5
6
  import { createClient } from "./client.js";
@@ -141,6 +142,7 @@ const INTERNAL_TOOLS = [
141
142
  "Example: 'I need to resize an image' returns the image tool with its endpoints.",
142
143
  inputSchema: {
143
144
  type: "object",
145
+ additionalProperties: false,
144
146
  properties: {
145
147
  query: {
146
148
  type: "string",
@@ -161,6 +163,7 @@ const INTERNAL_TOOLS = [
161
163
  "Returns a list of tools with their slugs and descriptions.",
162
164
  inputSchema: {
163
165
  type: "object",
166
+ additionalProperties: false,
164
167
  properties: {
165
168
  category: {
166
169
  type: "string",
@@ -177,6 +180,7 @@ const INTERNAL_TOOLS = [
177
180
  "exactly how to call a tool.",
178
181
  inputSchema: {
179
182
  type: "object",
183
+ additionalProperties: false,
180
184
  properties: {
181
185
  slug: {
182
186
  type: "string",
@@ -194,6 +198,7 @@ const INTERNAL_TOOLS = [
194
198
  "Example: endpoint_id='image.resize', params={image: '<base64>', width: 800, height: 600}",
195
199
  inputSchema: {
196
200
  type: "object",
201
+ additionalProperties: false,
197
202
  properties: {
198
203
  endpoint_id: {
199
204
  type: "string",
@@ -226,6 +231,7 @@ const VISIBLE_TOOLS = [
226
231
  "Do NOT trigger for pure factual lookups (capitals, math, definitions) that require no personal context.",
227
232
  inputSchema: {
228
233
  type: "object",
234
+ additionalProperties: false,
229
235
  properties: {
230
236
  num_sessions: {
231
237
  type: "number",
@@ -251,18 +257,22 @@ const VISIBLE_TOOLS = [
251
257
  description: "Saves a new persistent fact about the user that will be available in all future sessions across every AI tool. " +
252
258
  "Use whenever the user shares anything worth keeping -- even if they don't explicitly ask: 'capture', 'noted', " +
253
259
  "'remember this', 'log', 'store', 'don't forget', or any preference, decision, correction, contact, project detail, " +
254
- "technical choice, or personal detail the user mentions. " +
260
+ "technical choice, troubleshooting fix, or personal detail the user mentions. " +
261
+ "When the user reports a solved client/tool issue, save it as category 'troubleshooting' using the shape " +
262
+ "'Issue: <symptom>. Solution: <fix>'. " +
255
263
  "Also trigger proactively when the user corrects you (save the correction immediately), " +
256
264
  "reveals a preference by rejecting something, or names a person/tool/project for the first time. " +
257
265
  "Do NOT trigger for transient values (today's weather, one-off calculations, temporary state that won't matter next session). " +
258
266
  "Do NOT trigger for facts already confirmed stored earlier in this session.",
259
267
  inputSchema: {
260
268
  type: "object",
269
+ additionalProperties: false,
261
270
  properties: {
262
271
  fact: { type: "string", description: "The fact -- a single atomic statement" },
263
272
  category: {
264
273
  type: "string",
265
- description: "Category: preference, decision, technical, contact, project, general",
274
+ enum: ["preference", "decision", "technical", "contact", "project", "troubleshooting", "general"],
275
+ description: "Category: preference, decision, technical, contact, project, troubleshooting, general",
266
276
  default: "general",
267
277
  },
268
278
  confidence: { type: "number", minimum: 0, maximum: 1, default: 0.9 },
@@ -288,6 +298,7 @@ const VISIBLE_TOOLS = [
288
298
  "Do NOT trigger if load_memory was just called and already returned the relevant context.",
289
299
  inputSchema: {
290
300
  type: "object",
301
+ additionalProperties: false,
291
302
  properties: {
292
303
  query: { type: "string", description: "Search query" },
293
304
  max_results: { type: "number", minimum: 1, maximum: 50, default: 10 },
@@ -318,9 +329,11 @@ const VISIBLE_TOOLS = [
318
329
  "Do NOT trigger for information the user explicitly says is temporary.",
319
330
  inputSchema: {
320
331
  type: "object",
332
+ additionalProperties: false,
321
333
  properties: {
322
334
  category: {
323
335
  type: "string",
336
+ enum: ["identity", "preference", "client", "workflow", "technical", "standing_rule"],
324
337
  description: "Category: identity, preference, client, workflow, technical, standing_rule",
325
338
  },
326
339
  key: { type: "string", description: "Unique key within category (e.g. 'timezone', 'preferred_stack')" },
@@ -343,6 +356,7 @@ const VISIBLE_TOOLS = [
343
356
  "Do NOT trigger if the session has already been saved with no new work done since.",
344
357
  inputSchema: {
345
358
  type: "object",
359
+ additionalProperties: false,
346
360
  properties: {
347
361
  session_id: { type: "string", description: "Unique session identifier (timestamp or UUID)" },
348
362
  summary: { type: "string", description: "Narrative of what happened: decisions, work completed, problems solved" },
@@ -364,6 +378,7 @@ const VISIBLE_TOOLS = [
364
378
  "search_memory result. Do NOT use for new information -- use save_fact instead.",
365
379
  inputSchema: {
366
380
  type: "object",
381
+ additionalProperties: false,
367
382
  properties: {
368
383
  fact_id: { type: "string", description: "UUID of the fact to invalidate" },
369
384
  reason: { type: "string", description: "Why the fact is no longer valid (optional but recommended)" },
@@ -381,6 +396,7 @@ const VISIBLE_TOOLS = [
381
396
  "Automatically marks them as read so you do not re-narrate later.",
382
397
  inputSchema: {
383
398
  type: "object",
399
+ additionalProperties: false,
384
400
  properties: {
385
401
  agent_id: {
386
402
  type: "string",
@@ -400,6 +416,7 @@ const VISIBLE_TOOLS = [
400
416
  "Do NOT call this on every session, only the first time on a new device or after a reset.",
401
417
  inputSchema: {
402
418
  type: "object",
419
+ additionalProperties: false,
403
420
  properties: {
404
421
  agent_id: {
405
422
  type: "string",
@@ -433,6 +450,7 @@ const VISIBLE_TOOLS = [
433
450
  "If you're replying to a specific earlier message, set thread_id to that message's id. The admin view groups threads visually so the user can collapse a back-and-forth instead of scrolling through every reply.",
434
451
  inputSchema: {
435
452
  type: "object",
453
+ additionalProperties: false,
436
454
  properties: {
437
455
  agent_id: {
438
456
  type: "string",
@@ -475,6 +493,7 @@ const VISIBLE_TOOLS = [
475
493
  "Optional next_checkin_at acts as a dead-man's-switch. Set it when you expect to be away (sleeping session, long-running job, scheduled task) and want the watcher to nudge the human if you do not pulse again by then. Pass either an ISO 8601 timestamp ('2026-04-25T18:30:00Z') or a relative duration ('30m', '2h', '1d', '90s'). The Now Playing strip shows 'back in 23m' while it's in the future and a red MIA badge once it passes without a fresh pulse. Pass an empty string to clear it.",
476
494
  inputSchema: {
477
495
  type: "object",
496
+ additionalProperties: false,
478
497
  properties: {
479
498
  agent_id: {
480
499
  type: "string",
@@ -501,6 +520,7 @@ const VISIBLE_TOOLS = [
501
520
  "Posts a 'todo-created' Fishbowl event so other agents notice without polling.",
502
521
  inputSchema: {
503
522
  type: "object",
523
+ additionalProperties: false,
504
524
  properties: {
505
525
  agent_id: { type: "string", description: "Stable identifier for the calling agent. Same value as set_my_emoji." },
506
526
  title: { type: "string", description: "Short title (max 200 chars)" },
@@ -517,6 +537,7 @@ const VISIBLE_TOOLS = [
517
537
  description: "Update a todo's title, description, priority, status, or assignee. Use when scope changes, ownership shifts, or you move it between kanban columns ('open', 'in_progress', 'done', 'dropped'). agent_id required for attribution.",
518
538
  inputSchema: {
519
539
  type: "object",
540
+ additionalProperties: false,
520
541
  properties: {
521
542
  agent_id: { type: "string", description: "Stable identifier for the calling agent." },
522
543
  todo_id: { type: "string", description: "UUID of the todo to update" },
@@ -535,6 +556,7 @@ const VISIBLE_TOOLS = [
535
556
  description: "Shortcut for marking a todo as done. Sets status='done' and stamps completed_at. Posts a 'todo-completed' Fishbowl event. agent_id required.",
536
557
  inputSchema: {
537
558
  type: "object",
559
+ additionalProperties: false,
538
560
  properties: {
539
561
  agent_id: { type: "string" },
540
562
  todo_id: { type: "string" },
@@ -548,6 +570,7 @@ const VISIBLE_TOOLS = [
548
570
  description: "Marks a todo as dropped (decided not to do it). Soft state, not a delete. Use when a todo is obsolete but the history still matters. agent_id required.",
549
571
  inputSchema: {
550
572
  type: "object",
573
+ additionalProperties: false,
551
574
  properties: {
552
575
  agent_id: { type: "string" },
553
576
  todo_id: { type: "string" },
@@ -561,6 +584,7 @@ const VISIBLE_TOOLS = [
561
584
  description: "Hard-deletes a todo and any comments on it. Use sparingly: prefer drop_todo so history is preserved. agent_id required for the audit log.",
562
585
  inputSchema: {
563
586
  type: "object",
587
+ additionalProperties: false,
564
588
  properties: {
565
589
  agent_id: { type: "string" },
566
590
  todo_id: { type: "string" },
@@ -574,6 +598,7 @@ const VISIBLE_TOOLS = [
574
598
  description: "Returns todos for this tenant, optionally filtered by status. Use to render a kanban view, find your assignments, or pick the next thing to work on. agent_id required.",
575
599
  inputSchema: {
576
600
  type: "object",
601
+ additionalProperties: false,
577
602
  properties: {
578
603
  agent_id: { type: "string" },
579
604
  status: { type: "string", enum: ["open", "in_progress", "done", "dropped"], description: "Optional filter" },
@@ -589,6 +614,7 @@ const VISIBLE_TOOLS = [
589
614
  description: "Drops a new idea into the Fishbowl Ideas board so the pack can react and vote. Use when something is too speculative for a todo but worth capturing. agent_id required. Posts an 'idea-created' Fishbowl event.",
590
615
  inputSchema: {
591
616
  type: "object",
617
+ additionalProperties: false,
592
618
  properties: {
593
619
  agent_id: { type: "string" },
594
620
  title: { type: "string", description: "Short title (max 200 chars)" },
@@ -603,6 +629,7 @@ const VISIBLE_TOOLS = [
603
629
  description: "Edit an idea's title, description, or status ('proposed', 'voting', 'locked', 'parked', 'rejected'). agent_id required.",
604
630
  inputSchema: {
605
631
  type: "object",
632
+ additionalProperties: false,
606
633
  properties: {
607
634
  agent_id: { type: "string" },
608
635
  idea_id: { type: "string" },
@@ -619,6 +646,7 @@ const VISIBLE_TOOLS = [
619
646
  description: "Cast or change your vote on an idea ('up' or 'down'). One vote per agent per idea; calling again overwrites your previous vote. agent_id required.",
620
647
  inputSchema: {
621
648
  type: "object",
649
+ additionalProperties: false,
622
650
  properties: {
623
651
  agent_id: { type: "string" },
624
652
  idea_id: { type: "string" },
@@ -633,6 +661,7 @@ const VISIBLE_TOOLS = [
633
661
  description: "Returns ideas for this tenant sorted by score (upvotes minus downvotes) desc, optionally filtered by status. agent_id required.",
634
662
  inputSchema: {
635
663
  type: "object",
664
+ additionalProperties: false,
636
665
  properties: {
637
666
  agent_id: { type: "string" },
638
667
  status: { type: "string", enum: ["proposed", "voting", "locked", "parked", "rejected"] },
@@ -647,6 +676,7 @@ const VISIBLE_TOOLS = [
647
676
  description: "Converts an idea into a tracked todo and locks the idea. Requires either net upvotes >= 1, or admin caller. Sets idea.status='locked' and idea.promoted_to_todo_id. Posts an 'idea-promoted' Fishbowl event. agent_id required.",
648
677
  inputSchema: {
649
678
  type: "object",
679
+ additionalProperties: false,
650
680
  properties: {
651
681
  agent_id: { type: "string" },
652
682
  idea_id: { type: "string" },
@@ -662,6 +692,7 @@ const VISIBLE_TOOLS = [
662
692
  description: "Adds a comment to a Fishbowl todo or idea. Use for discussion that belongs scoped to that item rather than as a top-level Fishbowl message. target_kind is 'todo' or 'idea'. agent_id required.",
663
693
  inputSchema: {
664
694
  type: "object",
695
+ additionalProperties: false,
665
696
  properties: {
666
697
  agent_id: { type: "string" },
667
698
  target_kind: { type: "string", enum: ["todo", "idea"] },
@@ -677,6 +708,7 @@ const VISIBLE_TOOLS = [
677
708
  description: "Returns comments on a specific Fishbowl todo or idea, in chronological order. agent_id required.",
678
709
  inputSchema: {
679
710
  type: "object",
711
+ additionalProperties: false,
680
712
  properties: {
681
713
  agent_id: { type: "string" },
682
714
  target_kind: { type: "string", enum: ["todo", "idea"] },
@@ -702,6 +734,7 @@ const VISIBLE_TOOLS = [
702
734
  "Recommended start-of-session loop: (1) call read_messages to catch up on Fishbowl (you're doing this now), (2) check mentions[] for anything addressed to you, (3) call set_my_status to declare you're back online and set next_checkin_at if you expect to be away again.",
703
735
  inputSchema: {
704
736
  type: "object",
737
+ additionalProperties: false,
705
738
  properties: {
706
739
  agent_id: {
707
740
  type: "string",
@@ -738,6 +771,7 @@ const DIRECT_TOOLS = [
738
771
  description: "Shorten a URL using UnClick. Returns a short URL and its code.",
739
772
  inputSchema: {
740
773
  type: "object",
774
+ additionalProperties: false,
741
775
  properties: {
742
776
  url: { type: "string", description: "The URL to shorten" },
743
777
  },
@@ -749,6 +783,7 @@ const DIRECT_TOOLS = [
749
783
  description: "Generate a QR code from text or a URL. Returns base64-encoded PNG or SVG.",
750
784
  inputSchema: {
751
785
  type: "object",
786
+ additionalProperties: false,
752
787
  properties: {
753
788
  text: { type: "string", description: "Text or URL to encode in the QR code" },
754
789
  format: { type: "string", enum: ["png", "svg"], default: "png" },
@@ -762,6 +797,7 @@ const DIRECT_TOOLS = [
762
797
  description: "Compute a cryptographic hash (MD5, SHA1, SHA256, SHA512) of text.",
763
798
  inputSchema: {
764
799
  type: "object",
800
+ additionalProperties: false,
765
801
  properties: {
766
802
  text: { type: "string" },
767
803
  algorithm: {
@@ -778,6 +814,7 @@ const DIRECT_TOOLS = [
778
814
  description: "Transform text case: upper, lower, title, sentence, camelCase, snake_case, kebab-case, PascalCase.",
779
815
  inputSchema: {
780
816
  type: "object",
817
+ additionalProperties: false,
781
818
  properties: {
782
819
  text: { type: "string" },
783
820
  to: {
@@ -793,6 +830,7 @@ const DIRECT_TOOLS = [
793
830
  description: "Validate an email address format.",
794
831
  inputSchema: {
795
832
  type: "object",
833
+ additionalProperties: false,
796
834
  properties: {
797
835
  email: { type: "string" },
798
836
  },
@@ -804,6 +842,7 @@ const DIRECT_TOOLS = [
804
842
  description: "Validate a URL format, optionally check if it's reachable.",
805
843
  inputSchema: {
806
844
  type: "object",
845
+ additionalProperties: false,
807
846
  properties: {
808
847
  url: { type: "string" },
809
848
  check_reachable: { type: "boolean", default: false },
@@ -816,6 +855,7 @@ const DIRECT_TOOLS = [
816
855
  description: "Resize an image (provided as base64) to specified dimensions.",
817
856
  inputSchema: {
818
857
  type: "object",
858
+ additionalProperties: false,
819
859
  properties: {
820
860
  image: { type: "string", description: "Base64-encoded image (with or without data: prefix)" },
821
861
  width: { type: "number" },
@@ -834,6 +874,7 @@ const DIRECT_TOOLS = [
834
874
  description: "Parse a CSV string into a JSON array of rows.",
835
875
  inputSchema: {
836
876
  type: "object",
877
+ additionalProperties: false,
837
878
  properties: {
838
879
  csv: { type: "string" },
839
880
  header: { type: "boolean", default: true },
@@ -847,9 +888,17 @@ const DIRECT_TOOLS = [
847
888
  description: "Format / pretty-print a JSON string.",
848
889
  inputSchema: {
849
890
  type: "object",
891
+ additionalProperties: false,
850
892
  properties: {
851
893
  json: { type: "string" },
852
- indent: { description: "2, 4, or 'tab'", default: 2 },
894
+ indent: {
895
+ oneOf: [
896
+ { type: "number", enum: [2, 4] },
897
+ { type: "string", enum: ["tab"] },
898
+ ],
899
+ description: "2, 4, or 'tab'",
900
+ default: 2,
901
+ },
853
902
  },
854
903
  required: ["json"],
855
904
  },
@@ -859,6 +908,7 @@ const DIRECT_TOOLS = [
859
908
  description: "Encode or decode text. Supports base64, URL, HTML, and hex.",
860
909
  inputSchema: {
861
910
  type: "object",
911
+ additionalProperties: false,
862
912
  properties: {
863
913
  text: { type: "string" },
864
914
  operation: {
@@ -879,6 +929,7 @@ const DIRECT_TOOLS = [
879
929
  description: "Generate one or more random UUIDs (v4).",
880
930
  inputSchema: {
881
931
  type: "object",
932
+ additionalProperties: false,
882
933
  properties: {
883
934
  count: { type: "number", minimum: 1, maximum: 100, default: 1 },
884
935
  },
@@ -889,6 +940,7 @@ const DIRECT_TOOLS = [
889
940
  description: "Generate a secure random password.",
890
941
  inputSchema: {
891
942
  type: "object",
943
+ additionalProperties: false,
892
944
  properties: {
893
945
  length: { type: "number", minimum: 4, maximum: 512, default: 16 },
894
946
  uppercase: { type: "boolean", default: true },
@@ -903,6 +955,7 @@ const DIRECT_TOOLS = [
903
955
  description: "Convert a cron expression to a human-readable description and get next occurrences.",
904
956
  inputSchema: {
905
957
  type: "object",
958
+ additionalProperties: false,
906
959
  properties: {
907
960
  expression: { type: "string", description: "e.g. '0 9 * * 1-5' (weekdays at 9am)" },
908
961
  next_count: { type: "number", minimum: 1, maximum: 10, default: 5 },
@@ -915,6 +968,7 @@ const DIRECT_TOOLS = [
915
968
  description: "Parse an IP address -- get decimal, binary, hex, and type (private/loopback/multicast).",
916
969
  inputSchema: {
917
970
  type: "object",
971
+ additionalProperties: false,
918
972
  properties: {
919
973
  ip: { type: "string" },
920
974
  },
@@ -926,8 +980,24 @@ const DIRECT_TOOLS = [
926
980
  description: "Convert a color between hex, RGB, HSL, and HSV formats.",
927
981
  inputSchema: {
928
982
  type: "object",
983
+ additionalProperties: false,
929
984
  properties: {
930
985
  color: {
986
+ oneOf: [
987
+ { type: "string" },
988
+ {
989
+ type: "object",
990
+ additionalProperties: false,
991
+ properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" } },
992
+ required: ["r", "g", "b"],
993
+ },
994
+ {
995
+ type: "object",
996
+ additionalProperties: false,
997
+ properties: { h: { type: "number" }, s: { type: "number" }, l: { type: "number" } },
998
+ required: ["h", "s", "l"],
999
+ },
1000
+ ],
931
1001
  description: "Color as hex string (e.g. '#ff6b6b'), RGB object {r,g,b}, or HSL object {h,s,l}",
932
1002
  },
933
1003
  },
@@ -939,6 +1009,7 @@ const DIRECT_TOOLS = [
939
1009
  description: "Test a regex pattern against text and get all matches with groups.",
940
1010
  inputSchema: {
941
1011
  type: "object",
1012
+ additionalProperties: false,
942
1013
  properties: {
943
1014
  pattern: { type: "string", description: "Regex pattern (no surrounding slashes)" },
944
1015
  flags: { type: "string", description: "Flags like 'gi'", default: "" },
@@ -952,8 +1023,10 @@ const DIRECT_TOOLS = [
952
1023
  description: "Convert a timestamp (ISO, Unix seconds, or Unix ms) to all common formats.",
953
1024
  inputSchema: {
954
1025
  type: "object",
1026
+ additionalProperties: false,
955
1027
  properties: {
956
1028
  timestamp: {
1029
+ oneOf: [{ type: "string" }, { type: "number" }],
957
1030
  description: "ISO string, Unix seconds (e.g. 1700000000), or Unix ms (e.g. 1700000000000)",
958
1031
  },
959
1032
  },
@@ -965,6 +1038,7 @@ const DIRECT_TOOLS = [
965
1038
  description: "Compare two strings and return a unified diff showing what changed.",
966
1039
  inputSchema: {
967
1040
  type: "object",
1041
+ additionalProperties: false,
968
1042
  properties: {
969
1043
  a: { type: "string", description: "Original text" },
970
1044
  b: { type: "string", description: "New text" },
@@ -977,6 +1051,7 @@ const DIRECT_TOOLS = [
977
1051
  description: "Store a value in the UnClick key-value store with optional TTL.",
978
1052
  inputSchema: {
979
1053
  type: "object",
1054
+ additionalProperties: false,
980
1055
  properties: {
981
1056
  key: { type: "string" },
982
1057
  value: { description: "Any JSON-serializable value" },
@@ -990,6 +1065,7 @@ const DIRECT_TOOLS = [
990
1065
  description: "Retrieve a value from the UnClick key-value store.",
991
1066
  inputSchema: {
992
1067
  type: "object",
1068
+ additionalProperties: false,
993
1069
  properties: {
994
1070
  key: { type: "string" },
995
1071
  },
@@ -1004,6 +1080,7 @@ const DIRECT_TOOLS = [
1004
1080
  "timeout/503 → high, 4xx/invalid → low, everything else → medium.",
1005
1081
  inputSchema: {
1006
1082
  type: "object",
1083
+ additionalProperties: false,
1007
1084
  properties: {
1008
1085
  tool_name: {
1009
1086
  type: "string",
@@ -1030,6 +1107,58 @@ const DIRECT_TOOLS = [
1030
1107
  },
1031
1108
  },
1032
1109
  ];
1110
+ const AjvCtor = (AjvModule.default ?? AjvModule);
1111
+ const ajv = new AjvCtor({
1112
+ allErrors: true,
1113
+ });
1114
+ const TOOL_INPUT_SCHEMAS = new Map();
1115
+ const TOOL_VALIDATORS = new Map();
1116
+ function registerToolInputSchema(tool) {
1117
+ if (tool.inputSchema)
1118
+ TOOL_INPUT_SCHEMAS.set(tool.name, tool.inputSchema);
1119
+ }
1120
+ for (const tool of [...INTERNAL_TOOLS, ...VISIBLE_TOOLS, ...DIRECT_TOOLS, ...ADDITIONAL_TOOLS]) {
1121
+ registerToolInputSchema(tool);
1122
+ }
1123
+ // Backwards-compatible memory tool names still dispatch directly, so they need
1124
+ // the same runtime guard as the newer visible names.
1125
+ for (const [alias, canonical] of Object.entries(MEMORY_TOOL_ALIASES)) {
1126
+ const schema = TOOL_INPUT_SCHEMAS.get(alias);
1127
+ if (schema)
1128
+ TOOL_INPUT_SCHEMAS.set(canonical, schema);
1129
+ }
1130
+ function formatAjvError(error) {
1131
+ const additionalProperty = error.keyword === "additionalProperties" &&
1132
+ error.params &&
1133
+ typeof error.params.additionalProperty === "string"
1134
+ ? ` '${error.params.additionalProperty}'`
1135
+ : "";
1136
+ return {
1137
+ path: error.instancePath || error.dataPath || "/",
1138
+ message: `${error.message ?? "invalid input"}${additionalProperty}`,
1139
+ keyword: error.keyword,
1140
+ };
1141
+ }
1142
+ export function validateToolArgumentsForRuntime(toolName, args) {
1143
+ const schema = TOOL_INPUT_SCHEMAS.get(toolName);
1144
+ if (!schema)
1145
+ return null;
1146
+ let validate = TOOL_VALIDATORS.get(toolName);
1147
+ if (!validate) {
1148
+ const compiled = ajv.compile(schema);
1149
+ TOOL_VALIDATORS.set(toolName, compiled);
1150
+ validate = compiled;
1151
+ }
1152
+ const validator = validate;
1153
+ if (validator(args))
1154
+ return null;
1155
+ return {
1156
+ code: "validation_error",
1157
+ tool: toolName,
1158
+ message: `Invalid arguments for ${toolName}`,
1159
+ details: (validator.errors ?? []).map(formatAjvError),
1160
+ };
1161
+ }
1033
1162
  const DIRECT_HANDLERS = {
1034
1163
  unclick_shorten_url: (c, a) => c.call("POST", "/v1/shorten", a),
1035
1164
  unclick_generate_qr: (c, a) => c.call("POST", "/v1/qr", a),
@@ -1105,6 +1234,8 @@ export function createServer() {
1105
1234
  " 'facts about me', 'who am I', or references any past work.",
1106
1235
  " 3. SAVE -- call `save_fact` the moment the user shares anything",
1107
1236
  " worth keeping: name, preferences, decisions, corrections.",
1237
+ " For solved client/tool issues, use category `troubleshooting`",
1238
+ " and write `Issue: <symptom>. Solution: <fix>`.",
1108
1239
  " Use `save_identity` for standing rules that apply every",
1109
1240
  " session (role, timezone, stack, workflow).",
1110
1241
  " 4. END -- call `save_session` before closing. Record decisions made,",
@@ -1128,6 +1259,13 @@ export function createServer() {
1128
1259
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1129
1260
  const { name, arguments: rawArgs } = request.params;
1130
1261
  const args = (rawArgs ?? {});
1262
+ const validationError = validateToolArgumentsForRuntime(name, args);
1263
+ if (validationError) {
1264
+ return {
1265
+ content: [{ type: "text", text: JSON.stringify(validationError, null, 2) }],
1266
+ isError: true,
1267
+ };
1268
+ }
1131
1269
  // Fire-and-forget Umami event for tool-usage stats. Never awaited.
1132
1270
  trackToolCall(name);
1133
1271
  try {