wiki-viewer 1.4.3 → 1.5.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 (187) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +5 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/routes-manifest.json +34 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +8 -7
  24. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/app-paths-manifest.json +3 -0
  26. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/build-manifest.json +9 -0
  27. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/server-reference-manifest.json +4 -0
  28. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js +17 -0
  29. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.map +5 -0
  30. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -0
  31. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route_client-reference-manifest.js +3 -0
  32. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/app-paths-manifest.json +3 -0
  33. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/build-manifest.json +9 -0
  34. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/server-reference-manifest.json +4 -0
  35. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js +15 -0
  36. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.map +5 -0
  37. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -0
  38. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route_client-reference-manifest.js +3 -0
  39. package/.next/standalone/.next/server/app/api/agent/fs/move/route/app-paths-manifest.json +3 -0
  40. package/.next/standalone/.next/server/app/api/agent/fs/move/route/build-manifest.json +9 -0
  41. package/.next/standalone/.next/server/app/api/agent/fs/move/route/server-reference-manifest.json +4 -0
  42. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js +15 -0
  43. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.map +5 -0
  44. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -0
  45. package/.next/standalone/.next/server/app/api/agent/fs/move/route_client-reference-manifest.js +3 -0
  46. package/.next/standalone/.next/server/app/api/agent/fs/search/route/app-paths-manifest.json +3 -0
  47. package/.next/standalone/.next/server/app/api/agent/fs/search/route/build-manifest.json +9 -0
  48. package/.next/standalone/.next/server/app/api/agent/fs/search/route/server-reference-manifest.json +4 -0
  49. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js +15 -0
  50. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.map +5 -0
  51. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -0
  52. package/.next/standalone/.next/server/app/api/agent/fs/search/route_client-reference-manifest.js +3 -0
  53. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/agent/settings/route.js +1 -1
  55. package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/api/wiki/content/route.js +2 -2
  68. package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/api/wiki/file/route.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/api/wiki/move/route.js +3 -3
  72. package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/api/wiki/presence/route/app-paths-manifest.json +3 -0
  76. package/.next/standalone/.next/server/app/api/wiki/presence/route/build-manifest.json +9 -0
  77. package/.next/standalone/.next/server/app/api/wiki/presence/route/server-reference-manifest.json +4 -0
  78. package/.next/standalone/.next/server/app/api/wiki/presence/route.js +14 -0
  79. package/.next/standalone/.next/server/app/api/wiki/presence/route.js.map +5 -0
  80. package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -0
  81. package/.next/standalone/.next/server/app/api/wiki/presence/route_client-reference-manifest.js +3 -0
  82. package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
  86. package/.next/standalone/.next/server/app/index.html +1 -1
  87. package/.next/standalone/.next/server/app/index.rsc +2 -2
  88. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  89. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  90. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  91. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  92. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  93. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  94. package/.next/standalone/.next/server/app-paths-manifest.json +5 -0
  95. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0d2jbo5.js +3 -0
  96. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0ytral2.js +3 -0
  97. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0269d2e._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05f6x0i._.js +3 -0
  99. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06-dtd0._.js +3 -0
  100. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06sek6_._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/[root-of-the-server]__09s9qkd._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d75a5q._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_da6q._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e7si9p._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hhhvm7._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kk26wc._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kur4ro._.js +15 -0
  108. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0p12-6~._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qc2~6l._.js +15 -0
  110. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s~i040._.js +3 -0
  111. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w47wil._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xq124z._.js +3 -0
  113. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l..bxz._.js → [root-of-the-server]__0xz-_0v._.js} +2 -2
  114. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10w2q~o._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/_079xuln._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_file_[___path]_route_actions_0gb11t~.js +3 -0
  117. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_ls_[[___path]]_route_actions_0wfg4nl.js +3 -0
  118. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_move_route_actions_0z34mqq.js +3 -0
  119. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_search_route_actions_0ut_ug..js +3 -0
  120. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wiki_presence_route_actions_0x9d7f4.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_diagram-OG6HWLK6_mjs_0t5sus6._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_sankeyDiagram-5OEKKPKP_mjs_11ifrnu._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +2 -2
  124. package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0y9k6h~._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0znncy9._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_12-ni1n._.js +1 -1
  127. package/.next/standalone/.next/server/functions-config-manifest.json +5 -0
  128. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  129. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  130. package/.next/standalone/.next/server/pages/404.html +1 -1
  131. package/.next/standalone/.next/server/pages/500.html +1 -1
  132. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  133. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  134. package/.next/standalone/.next/static/chunks/{11s9zwr7edrs7.js → 14luv~jq7e6-n.js} +12 -12
  135. package/.next/standalone/README.md +79 -16
  136. package/.next/standalone/agents/bootstrap-prompt.md +12 -1
  137. package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +199 -6
  138. package/.next/standalone/docs/file-vs-collab-authority.md +124 -0
  139. package/.next/standalone/package.json +1 -1
  140. package/.next/standalone/packages/wiki-viewer-mcp/README.md +194 -0
  141. package/.next/standalone/packages/wiki-viewer-mcp/dist/http-client.js +220 -0
  142. package/.next/standalone/packages/wiki-viewer-mcp/dist/index.js +472 -0
  143. package/.next/standalone/packages/wiki-viewer-mcp/dist/register.js +94 -0
  144. package/.next/standalone/packages/wiki-viewer-mcp/dist/state-cache.js +26 -0
  145. package/.next/standalone/packages/wiki-viewer-mcp/package-lock.json +1716 -0
  146. package/.next/standalone/packages/wiki-viewer-mcp/package.json +41 -0
  147. package/.next/standalone/packages/wiki-viewer-mcp/src/http-client.ts +344 -0
  148. package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +575 -0
  149. package/.next/standalone/packages/wiki-viewer-mcp/src/register.ts +144 -0
  150. package/.next/standalone/packages/wiki-viewer-mcp/src/state-cache.ts +45 -0
  151. package/.next/standalone/packages/wiki-viewer-mcp/tsconfig.json +19 -0
  152. package/.next/standalone/pnpm-lock.yaml +9 -9
  153. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +11 -1
  154. package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +344 -0
  155. package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +147 -0
  156. package/.next/standalone/src/app/api/agent/fs/move/route.ts +125 -0
  157. package/.next/standalone/src/app/api/agent/fs/search/route.ts +236 -0
  158. package/.next/standalone/src/app/api/agent/register/route.ts +3 -3
  159. package/.next/standalone/src/app/api/agents/install/route.ts +44 -2
  160. package/.next/standalone/src/app/api/wiki/move/route.ts +10 -1
  161. package/.next/standalone/src/app/api/wiki/presence/route.ts +55 -0
  162. package/.next/standalone/src/components/ai-panel/ai-panel.tsx +49 -0
  163. package/.next/standalone/src/components/editor/editor.tsx +38 -0
  164. package/.next/standalone/src/lib/proof/audit.ts +79 -0
  165. package/.next/standalone/src/lib/proof/auth.ts +2 -2
  166. package/.next/standalone/src/lib/proof/collab-state.ts +79 -0
  167. package/.next/standalone/src/lib/proof/lease.ts +104 -0
  168. package/.next/standalone/src/lib/proof/ops-applier.ts +74 -25
  169. package/.next/standalone/src/lib/proof/raw-fs.ts +198 -0
  170. package/.next/standalone/src/lib/proof/registry.ts +1 -1
  171. package/.next/standalone/src/lib/proof/sidecar.ts +38 -1
  172. package/.next/standalone/src/lib/proof/types.ts +13 -1
  173. package/.next/standalone/src/tests/proof/agent-fs.test.ts +824 -0
  174. package/.next/standalone/src/tests/proof/agents-install.test.ts +53 -0
  175. package/.next/standalone/src/tests/proof/collab-state.test.ts +499 -0
  176. package/.next/standalone/src/tests/proof/reconcile-sidecar.test.ts +195 -0
  177. package/.next/standalone/src/tests/proof/sidecar-lifecycle.test.ts +177 -0
  178. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  179. package/README.md +79 -16
  180. package/package.json +1 -1
  181. package/.next/standalone/.next/server/chunks/[root-of-the-server]__09~~-nd._.js +0 -3
  182. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w87kiz._.js +0 -3
  183. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12ssbhv._.js +0 -3
  184. package/.next/standalone/.next/server/chunks/node_modules__pnpm_0g0fhfk._.js +0 -3
  185. /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 19j4-_brJLoulXAMekpM2}/_buildManifest.js +0 -0
  186. /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 19j4-_brJLoulXAMekpM2}/_clientMiddlewareManifest.js +0 -0
  187. /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 19j4-_brJLoulXAMekpM2}/_ssgManifest.js +0 -0
@@ -0,0 +1,575 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * wiki-viewer-mcp — MCP filesystem adapter for wiki-viewer agent API.
4
+ *
5
+ * Maps standard MCP filesystem tools onto the wiki-viewer HTTP agent API
6
+ * described in docs/agent-fs-plan.md §2 + §3.5 + §6.
7
+ *
8
+ * Configuration (env vars):
9
+ * WIKI_VIEWER_URL Base URL of wiki-viewer instance (required)
10
+ * WIKI_VIEWER_TOKEN Bearer token from TOFU registration (required)
11
+ * WIKI_VIEWER_AGENT_ID X-Agent-Id header value (required)
12
+ *
13
+ * Mode-awareness:
14
+ * Before any raw write to a .md file, the shim checks the cached
15
+ * X-Collab-State from the last read. If "active", it blocks the write
16
+ * and instructs the agent to use Tier-2 block-ops instead.
17
+ * The server also enforces this (409 COLLAB_ACTIVE), which the shim
18
+ * surfaces cleanly.
19
+ */
20
+
21
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
+ import {
24
+ CallToolRequestSchema,
25
+ ListToolsRequestSchema,
26
+ } from "@modelcontextprotocol/sdk/types.js";
27
+ import * as z from "zod";
28
+
29
+ import { parseArgs } from "node:util";
30
+ import { WikiViewerClient, IfMatchError, CollabActiveError, WikiViewerError } from "./http-client.js";
31
+ import * as stateCache from "./state-cache.js";
32
+ import {
33
+ register,
34
+ RegisterScope,
35
+ RegistrationDeniedError,
36
+ RegistrationExpiredError,
37
+ RegistrationTimeoutError,
38
+ } from "./register.js";
39
+
40
+ // ─── Config ───────────────────────────────────────────────────────────────────
41
+
42
+ function requireEnv(name: string): string {
43
+ const v = process.env[name];
44
+ if (!v) throw new Error(`Missing required env var: ${name}`);
45
+ return v;
46
+ }
47
+
48
+ export function createClient(overrides?: {
49
+ baseUrl?: string;
50
+ token?: string;
51
+ agentId?: string;
52
+ fetch?: typeof fetch;
53
+ }): WikiViewerClient {
54
+ return new WikiViewerClient({
55
+ baseUrl: overrides?.baseUrl ?? requireEnv("WIKI_VIEWER_URL"),
56
+ token: overrides?.token ?? requireEnv("WIKI_VIEWER_TOKEN"),
57
+ agentId: overrides?.agentId ?? requireEnv("WIKI_VIEWER_AGENT_ID"),
58
+ fetch: overrides?.fetch,
59
+ });
60
+ }
61
+
62
+ // ─── Tool schemas ─────────────────────────────────────────────────────────────
63
+
64
+ const ReadFileInput = z.object({
65
+ path: z.string().describe("File path relative to wiki root"),
66
+ range: z.string().optional().describe("HTTP Range header value, e.g. 'bytes=0-1023'"),
67
+ });
68
+
69
+ const WriteFileInput = z.object({
70
+ path: z.string().describe("File path relative to wiki root"),
71
+ content: z.string().describe("File content (text)"),
72
+ mkdirs: z.boolean().optional().describe("Create parent directories if missing"),
73
+ force: z.boolean().optional().describe("Skip If-Match guard (audited)"),
74
+ ifCollabMatch: z.number().optional().describe("If-Collab-Match revision (for tracked .md)"),
75
+ });
76
+
77
+ const EditFileInput = z.object({
78
+ path: z.string().describe("File path relative to wiki root"),
79
+ find: z.string().describe("Exact string to find (first occurrence)"),
80
+ replace: z.string().describe("Replacement string"),
81
+ });
82
+
83
+ const ListDirectoryInput = z.object({
84
+ path: z.string().describe("Directory path relative to wiki root"),
85
+ recursive: z.boolean().optional().describe("List recursively"),
86
+ depth: z.number().optional().describe("Max depth for recursive listing"),
87
+ limit: z.number().optional().describe("Max entries to return"),
88
+ });
89
+
90
+ const SearchInput = z.object({
91
+ kind: z.enum(["grep", "glob"]).describe("grep = text search, glob = path pattern"),
92
+ query: z.string().describe("Search query or glob pattern"),
93
+ path: z.string().optional().describe("Root path to search within"),
94
+ glob: z.string().optional().describe("File glob filter for grep"),
95
+ limit: z.number().optional().describe("Max matches to return"),
96
+ });
97
+
98
+ const MoveFileInput = z.object({
99
+ from: z.string().describe("Source path"),
100
+ to: z.string().describe("Destination path"),
101
+ });
102
+
103
+ const DeleteFileInput = z.object({
104
+ path: z.string().describe("File path to delete"),
105
+ recursive: z.boolean().optional().describe("Delete directory recursively"),
106
+ force: z.boolean().optional().describe("Skip If-Match guard (audited)"),
107
+ });
108
+
109
+ // ─── Tool definitions ─────────────────────────────────────────────────────────
110
+
111
+ export const TOOLS = [
112
+ {
113
+ name: "read_file",
114
+ description:
115
+ "Read a file from the wiki-viewer instance. Returns content and metadata. " +
116
+ "Captures sha256 (ETag) and X-Collab-State for subsequent writes. " +
117
+ "Always read before writing to get the current sha.",
118
+ inputSchema: zodToJsonSchema(ReadFileInput),
119
+ },
120
+ {
121
+ name: "write_file",
122
+ description:
123
+ "Write (create or overwrite) a file. " +
124
+ "If you have previously read the file, the last known sha is sent as If-Match automatically — " +
125
+ "you get a 412 error if the file changed since your read. " +
126
+ "WARNING: for .md files with X-Collab-State 'active', raw writes are blocked — " +
127
+ "use wiki-viewer block-ops (Tier 2) instead so the human can review your changes.",
128
+ inputSchema: zodToJsonSchema(WriteFileInput),
129
+ },
130
+ {
131
+ name: "edit_file",
132
+ description:
133
+ "Edit a file by exact string replacement (first occurrence). " +
134
+ "Implemented client-side as: read → str-replace → PUT with If-Match. " +
135
+ "Returns an error if the find string is not found or the file is collab-active. " +
136
+ "For .md files, prefer block-ops if X-Collab-State is 'active'.",
137
+ inputSchema: zodToJsonSchema(EditFileInput),
138
+ },
139
+ {
140
+ name: "list_directory",
141
+ description: "List directory contents. Scope-filtered; .proof/ is hidden.",
142
+ inputSchema: zodToJsonSchema(ListDirectoryInput),
143
+ },
144
+ {
145
+ name: "search",
146
+ description:
147
+ "Search files. kind='grep' searches file contents; kind='glob' matches paths. " +
148
+ "Server-side — avoids round-trip explosion from ls+read patterns.",
149
+ inputSchema: zodToJsonSchema(SearchInput),
150
+ },
151
+ {
152
+ name: "move_file",
153
+ description:
154
+ "Move or rename a file. Sidecar (.proof/*.json) is moved automatically for .md files.",
155
+ inputSchema: zodToJsonSchema(MoveFileInput),
156
+ },
157
+ {
158
+ name: "delete_file",
159
+ description:
160
+ "Delete a file. Requires 'delete' scope. You must have read the file first " +
161
+ "(its sha is sent as If-Match). .md sidecars are removed automatically.",
162
+ inputSchema: zodToJsonSchema(DeleteFileInput),
163
+ },
164
+ ] as const;
165
+
166
+ // ─── Handler helpers ──────────────────────────────────────────────────────────
167
+
168
+ function isMd(path: string): boolean {
169
+ return path.endsWith(".md") || path.endsWith(".markdown");
170
+ }
171
+
172
+ function collabActiveMessage(path: string, snapshotUrl: string | null): string {
173
+ const tier2 = snapshotUrl
174
+ ? `\nTier-2 block-ops snapshot: ${snapshotUrl}`
175
+ : "";
176
+ return (
177
+ `⚠️ COLLAB ACTIVE: "${path}" is being actively collaborated on by a human.\n` +
178
+ `Raw writes are blocked to protect pending review artifacts.\n` +
179
+ `→ Use wiki-viewer Tier-2 block-ops (POST /api/agent/files/<path>.md with block.replace / suggestion.*) so your edit becomes a reviewable suggestion.${tier2}\n` +
180
+ `→ Alternatively, re-read the file and pass ifCollabMatch with the current X-Collab-Revision if you have confirmed with the human that a direct raw write is intended.`
181
+ );
182
+ }
183
+
184
+ function ifMatchMismatchMessage(path: string): string {
185
+ return (
186
+ `412 If-Match mismatch for "${path}" — the file changed since you last read it.\n` +
187
+ `Re-read the file with read_file to get the current content and sha, then retry.`
188
+ );
189
+ }
190
+
191
+ type ToolResult = { content: Array<{ type: "text"; text: string }> };
192
+
193
+ function ok(text: string): ToolResult {
194
+ return { content: [{ type: "text", text }] };
195
+ }
196
+
197
+ function err(text: string): ToolResult {
198
+ return { content: [{ type: "text", text: `ERROR: ${text}` }] };
199
+ }
200
+
201
+ /**
202
+ * Check cached collab state before a write.
203
+ * Returns a blocking error message if write should not proceed, or null if ok.
204
+ */
205
+ function checkCollabBlock(path: string): string | null {
206
+ if (!isMd(path)) return null;
207
+ const cached = stateCache.get(path);
208
+ if (!cached) return null; // no prior read — let server enforce
209
+ if (cached.collabState === "active") {
210
+ return collabActiveMessage(path, cached.collabSnapshot);
211
+ }
212
+ return null;
213
+ }
214
+
215
+ // ─── Server factory ───────────────────────────────────────────────────────────
216
+
217
+ export function createServer(client: WikiViewerClient): Server {
218
+ const server = new Server(
219
+ { name: "wiki-viewer-mcp", version: "0.1.0" },
220
+ { capabilities: { tools: {} } },
221
+ );
222
+
223
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
224
+ tools: TOOLS,
225
+ }));
226
+
227
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
228
+ const { name, arguments: args } = request.params;
229
+
230
+ try {
231
+ switch (name) {
232
+ // ── read_file ───────────────────────────────────────────────────────
233
+ case "read_file": {
234
+ const { path, range } = ReadFileInput.parse(args);
235
+ const result = await client.readFile(path, range);
236
+
237
+ const collabNote = result.collabState !== "not-markdown"
238
+ ? `\nX-Collab-State: ${result.collabState}` +
239
+ (result.collabRevision !== null ? `\nX-Collab-Revision: ${result.collabRevision}` : "") +
240
+ (result.collabSnapshot ? `\nX-Collab-Snapshot: ${result.collabSnapshot}` : "") +
241
+ (result.collabState === "active"
242
+ ? "\n⚠️ File is COLLAB ACTIVE — use block-ops for edits so human can review."
243
+ : result.collabState === "tracked"
244
+ ? "\nFile is tracked — prefer block-ops for prose/semantic edits."
245
+ : "")
246
+ : "";
247
+
248
+ if (result.text !== null) {
249
+ return ok(
250
+ `File: ${path}\nSize: ${result.size} bytes | ETag: ${result.sha256}${collabNote}\n\n${result.text}`,
251
+ );
252
+ } else {
253
+ return ok(
254
+ `File: ${path}\nSize: ${result.size} bytes | ETag: ${result.sha256}\nContent-Type: ${result.contentType} (binary — use range reads for partial content)${collabNote}`,
255
+ );
256
+ }
257
+ }
258
+
259
+ // ── write_file ──────────────────────────────────────────────────────
260
+ case "write_file": {
261
+ const { path, content, mkdirs, force, ifCollabMatch } = WriteFileInput.parse(args);
262
+
263
+ // Client-side collab guard
264
+ const block = checkCollabBlock(path);
265
+ if (block) return err(block);
266
+
267
+ const cached = stateCache.get(path);
268
+ const ifMatch = cached?.sha256;
269
+
270
+ const result = await client.writeFile(path, content, {
271
+ ifMatch,
272
+ mkdirs,
273
+ force,
274
+ ifCollabMatch,
275
+ });
276
+
277
+ return ok(
278
+ `Written: ${path}\nSha256: ${result.sha256}\nSize: ${result.size} bytes\n${result.created ? "Created (new file)" : "Overwritten"}`,
279
+ );
280
+ }
281
+
282
+ // ── edit_file ───────────────────────────────────────────────────────
283
+ case "edit_file": {
284
+ const { path, find, replace } = EditFileInput.parse(args);
285
+
286
+ // Client-side collab guard
287
+ const block = checkCollabBlock(path);
288
+ if (block) return err(block);
289
+
290
+ // Read → transform → PUT with If-Match
291
+ const readResult = await client.readFile(path);
292
+ if (readResult.text === null) {
293
+ return err(`edit_file: "${path}" appears to be binary — cannot do text replacement.`);
294
+ }
295
+
296
+ // Check collab state from fresh read
297
+ if (readResult.collabState === "active") {
298
+ return err(collabActiveMessage(path, readResult.collabSnapshot));
299
+ }
300
+
301
+ if (!readResult.text.includes(find)) {
302
+ return err(
303
+ `edit_file: string not found in "${path}".\n` +
304
+ `Search for: ${JSON.stringify(find)}\n` +
305
+ `Tip: re-read the file to see current content.`,
306
+ );
307
+ }
308
+
309
+ const newContent = readResult.text.replace(find, replace);
310
+ const writeResult = await client.writeFile(path, newContent, {
311
+ ifMatch: readResult.sha256,
312
+ });
313
+
314
+ return ok(
315
+ `Edited: ${path}\nReplaced ${JSON.stringify(find)} → ${JSON.stringify(replace)}\nNew sha256: ${writeResult.sha256}`,
316
+ );
317
+ }
318
+
319
+ // ── list_directory ──────────────────────────────────────────────────
320
+ case "list_directory": {
321
+ const { path, recursive, depth, limit } = ListDirectoryInput.parse(args);
322
+ const entries = await client.listDirectory(path, { recursive, depth, limit });
323
+ const lines = entries.map(
324
+ (e) =>
325
+ `${e.type === "directory" ? "DIR " : "FILE"} ${e.path}` +
326
+ (e.size !== null ? ` (${e.size}b)` : "") +
327
+ (e.mtime ? ` [${e.mtime}]` : ""),
328
+ );
329
+ return ok(lines.length > 0 ? lines.join("\n") : "(empty directory)");
330
+ }
331
+
332
+ // ── search ──────────────────────────────────────────────────────────
333
+ case "search": {
334
+ const body = SearchInput.parse(args);
335
+ const result = await client.search(body);
336
+ if (result.matches.length === 0) {
337
+ return ok("No matches found.");
338
+ }
339
+ const lines = result.matches.map((m) => {
340
+ if (result.kind === "grep" && m.line !== undefined) {
341
+ return `${m.path}:${m.line}: ${m.text ?? ""}`;
342
+ }
343
+ return m.path;
344
+ });
345
+ return ok(`${result.matches.length} match(es):\n${lines.join("\n")}`);
346
+ }
347
+
348
+ // ── move_file ───────────────────────────────────────────────────────
349
+ case "move_file": {
350
+ const { from, to } = MoveFileInput.parse(args);
351
+ const cached = stateCache.get(from);
352
+ await client.moveFile(from, to, cached?.sha256);
353
+ return ok(`Moved: ${from} → ${to}`);
354
+ }
355
+
356
+ // ── delete_file ─────────────────────────────────────────────────────
357
+ case "delete_file": {
358
+ const { path, recursive, force } = DeleteFileInput.parse(args);
359
+ const cached = stateCache.get(path);
360
+ if (!cached?.sha256) {
361
+ return err(
362
+ `delete_file: no cached sha for "${path}" — read the file first with read_file so the current sha can be sent as If-Match.`,
363
+ );
364
+ }
365
+ await client.deleteFile(path, cached.sha256, { recursive, force });
366
+ return ok(`Deleted: ${path}`);
367
+ }
368
+
369
+ default:
370
+ return err(`Unknown tool: ${name}`);
371
+ }
372
+ } catch (e) {
373
+ if (e instanceof IfMatchError) {
374
+ return err(ifMatchMismatchMessage(e.message.match(/"([^"]+)"/)?.at(1) ?? "file"));
375
+ }
376
+ if (e instanceof CollabActiveError) {
377
+ return err(collabActiveMessage(e.message, e.snapshotUrl));
378
+ }
379
+ if (e instanceof WikiViewerError) {
380
+ return err(`wiki-viewer error ${e.status} (${e.code}): ${e.message}`);
381
+ }
382
+ if (e instanceof z.ZodError) {
383
+ return err(`Invalid arguments: ${e.message}`);
384
+ }
385
+ throw e; // unexpected — let MCP transport handle
386
+ }
387
+ });
388
+
389
+ return server;
390
+ }
391
+
392
+ // ─── Inline minimal JSON-schema conversion ────────────────────────────────────
393
+ // Uses Zod v4 _def.type discriminator (v4 removed _def.typeName).
394
+
395
+ type AnyZodVal = { _def: { type: string; description?: string; innerType?: AnyZodVal; entries?: Record<string, string>; options?: string[] }; description?: string; options?: string[]; unwrap?: () => AnyZodVal };
396
+
397
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
398
+ function zodToJsonSchema(schema: z.ZodObject<any>): Record<string, unknown> {
399
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
400
+ const shape = schema.shape as Record<string, any>;
401
+ const properties: Record<string, unknown> = {};
402
+ const required: string[] = [];
403
+
404
+ for (const [key, val] of Object.entries(shape)) {
405
+ const v = val as AnyZodVal;
406
+ const isOptional = v._def.type === "optional";
407
+ const inner: AnyZodVal = isOptional ? (v.unwrap ? v.unwrap() : v._def.innerType ?? v) : v;
408
+
409
+ properties[key] = buildJsonSchema(inner);
410
+ if (!isOptional) required.push(key);
411
+ }
412
+
413
+ return { type: "object", properties, required };
414
+ }
415
+
416
+ function buildJsonSchema(val: AnyZodVal): Record<string, unknown> {
417
+ const desc = val.description ?? val._def.description;
418
+ const withDesc = (s: Record<string, unknown>) => { if (desc) s["description"] = desc; return s; };
419
+
420
+ switch (val._def.type) {
421
+ case "string": return withDesc({ type: "string" });
422
+ case "number": return withDesc({ type: "number" });
423
+ case "boolean": return withDesc({ type: "boolean" });
424
+ case "enum": {
425
+ const opts = val.options ?? Object.keys(val._def.entries ?? {});
426
+ return withDesc({ type: "string", enum: opts });
427
+ }
428
+ case "optional": return buildJsonSchema(val.unwrap ? val.unwrap() : val._def.innerType ?? val);
429
+ default: return withDesc({ type: "string" });
430
+ }
431
+ }
432
+
433
+ // ─── Entrypoints ─────────────────────────────────────────────────────────────
434
+
435
+ async function main() {
436
+ const client = createClient();
437
+ const server = createServer(client);
438
+ const transport = new StdioServerTransport();
439
+ await server.connect(transport);
440
+ // Server runs until stdin closes
441
+ }
442
+
443
+ /**
444
+ * CLI entry: routes to `register` subcommand or MCP stdio server.
445
+ * Only called when the file is the actual entry point (not imported in tests).
446
+ */
447
+ async function runCli() {
448
+ // Detect subcommand: first positional arg
449
+ const positional = process.argv.slice(2).filter((a) => !a.startsWith("-"));
450
+ const subcommand = positional[0];
451
+
452
+ if (subcommand === "register") {
453
+ await runRegister();
454
+ } else {
455
+ await main();
456
+ }
457
+ }
458
+
459
+ async function runRegister() {
460
+ const { values } = parseArgs({
461
+ args: process.argv.slice(2),
462
+ options: {
463
+ url: { type: "string" },
464
+ id: { type: "string" },
465
+ name: { type: "string" },
466
+ "scope-paths": { type: "string", default: "**/*" },
467
+ ops: { type: "string", default: "read,mutate" },
468
+ timeout: { type: "string", default: "300" },
469
+ },
470
+ allowPositionals: true,
471
+ });
472
+
473
+ if (!values.url) {
474
+ console.error("Error: --url is required (e.g. https://notes.example.com)");
475
+ process.exit(1);
476
+ }
477
+ if (!values.id) {
478
+ console.error("Error: --id is required (e.g. ai:myagent)");
479
+ process.exit(1);
480
+ }
481
+ if (!values.name) {
482
+ console.error("Error: --name is required (e.g. \"My Agent\")");
483
+ process.exit(1);
484
+ }
485
+ if (!values.id.match(/^ai:[a-z][a-z0-9-]{0,30}$/)) {
486
+ console.error("Error: --id must match ^ai:[a-z][a-z0-9-]{0,30}$ (e.g. ai:myagent)");
487
+ process.exit(1);
488
+ }
489
+
490
+ const scopePaths = (values["scope-paths"] ?? "**/*").split(",").map((p) => p.trim());
491
+ const rawOps = (values.ops ?? "read,mutate").split(",").map((o) => o.trim());
492
+ const validOps = ["read", "mutate", "delete"] as const;
493
+ const ops = rawOps.filter((o): o is typeof validOps[number] => (validOps as readonly string[]).includes(o));
494
+ if (ops.length === 0) {
495
+ console.error("Error: --ops must include at least one of: read, mutate, delete");
496
+ process.exit(1);
497
+ }
498
+ const scope: RegisterScope = { paths: scopePaths, ops };
499
+ const timeoutMs = parseInt(values.timeout ?? "300", 10) * 1000;
500
+
501
+ console.log(`Registering agent ${values.id} with ${values.url} …`);
502
+ console.log(`Scope: paths=${JSON.stringify(scopePaths)}, ops=${JSON.stringify(ops)}`);
503
+ console.log();
504
+
505
+ try {
506
+ const result = await register({
507
+ baseUrl: values.url,
508
+ id: values.id,
509
+ displayName: values.name,
510
+ scope,
511
+ timeoutMs,
512
+ onPending: (_id, attempt) => {
513
+ if (attempt === 1) {
514
+ console.log(
515
+ `⏳ Waiting for approval. Open the wiki-viewer AI Panel and approve agent "${values.id}".`,
516
+ );
517
+ } else if (attempt % 10 === 0) {
518
+ console.log(` Still waiting… (${attempt * 3}s elapsed)`);
519
+ }
520
+ },
521
+ });
522
+
523
+ console.log();
524
+ console.log("✅ Approved!");
525
+ console.log();
526
+ console.log(`Agent ID : ${result.agentId}`);
527
+ console.log(`Token : ${result.token}`);
528
+ console.log();
529
+ console.log("Paste this into your mcp.json:");
530
+ console.log();
531
+ console.log(JSON.stringify({
532
+ servers: {
533
+ "wiki-viewer": {
534
+ command: "npx",
535
+ args: ["wiki-viewer-mcp"],
536
+ env: {
537
+ WIKI_VIEWER_URL: values.url,
538
+ WIKI_VIEWER_TOKEN: result.token,
539
+ WIKI_VIEWER_AGENT_ID: result.agentId,
540
+ },
541
+ },
542
+ },
543
+ }, null, 2));
544
+ process.exit(0);
545
+ } catch (e) {
546
+ if (
547
+ e instanceof RegistrationDeniedError ||
548
+ e instanceof RegistrationExpiredError ||
549
+ e instanceof RegistrationTimeoutError
550
+ ) {
551
+ console.error(`\n❌ ${e.message}`);
552
+ } else {
553
+ console.error("\n❌ Unexpected error:", e);
554
+ }
555
+ process.exit(1);
556
+ }
557
+ }
558
+
559
+ // Only auto-start when this file is the entry point, not when imported in tests.
560
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\/dist\//, "/src/").replace(/\.js$/, ".ts"))) {
561
+ runCli().catch((e) => {
562
+ console.error("wiki-viewer-mcp fatal:", e);
563
+ process.exit(1);
564
+ });
565
+ } else if (
566
+ // Also run when invoked as the compiled binary
567
+ process.argv[1] &&
568
+ (process.argv[1].endsWith("wiki-viewer-mcp") || process.argv[1].endsWith("index.js")) &&
569
+ !process.argv[1].includes("__tests__")
570
+ ) {
571
+ runCli().catch((e) => {
572
+ console.error("wiki-viewer-mcp fatal:", e);
573
+ process.exit(1);
574
+ });
575
+ }