@chnxthr/knowledge-mcp 0.0.0-stage → 0.3.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/LICENSE +21 -0
  2. package/README.md +37 -2
  3. package/dist/bootstrap.d.ts +1 -0
  4. package/dist/bootstrap.js +18 -0
  5. package/dist/bootstrap.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +5 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/core/auth.d.ts +38 -0
  10. package/dist/core/auth.js +93 -0
  11. package/dist/core/auth.js.map +1 -0
  12. package/dist/core/config.d.ts +259 -0
  13. package/dist/core/config.js +172 -0
  14. package/dist/core/config.js.map +1 -0
  15. package/dist/core/context.d.ts +25 -0
  16. package/dist/core/context.js +37 -0
  17. package/dist/core/context.js.map +1 -0
  18. package/dist/core/env.d.ts +2 -0
  19. package/dist/core/env.js +5 -0
  20. package/dist/core/env.js.map +1 -0
  21. package/dist/core/log_mask.d.ts +10 -0
  22. package/dist/core/log_mask.js +88 -0
  23. package/dist/core/log_mask.js.map +1 -0
  24. package/dist/core/oauth.d.ts +46 -0
  25. package/dist/core/oauth.js +507 -0
  26. package/dist/core/oauth.js.map +1 -0
  27. package/dist/core/paths.d.ts +5 -0
  28. package/dist/core/paths.js +47 -0
  29. package/dist/core/paths.js.map +1 -0
  30. package/dist/core/scope.d.ts +28 -0
  31. package/dist/core/scope.js +54 -0
  32. package/dist/core/scope.js.map +1 -0
  33. package/dist/core/upstream.d.ts +30 -0
  34. package/dist/core/upstream.js +164 -0
  35. package/dist/core/upstream.js.map +1 -0
  36. package/dist/index.d.ts +23 -0
  37. package/dist/index.js +21 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/plugin/define.d.ts +144 -0
  40. package/dist/plugin/define.js +44 -0
  41. package/dist/plugin/define.js.map +1 -0
  42. package/dist/plugin/load.d.ts +2 -0
  43. package/dist/plugin/load.js +41 -0
  44. package/dist/plugin/load.js.map +1 -0
  45. package/dist/plugin/ping.d.ts +9 -0
  46. package/dist/plugin/ping.js +29 -0
  47. package/dist/plugin/ping.js.map +1 -0
  48. package/dist/plugin/registry.d.ts +62 -0
  49. package/dist/plugin/registry.js +248 -0
  50. package/dist/plugin/registry.js.map +1 -0
  51. package/dist/plugin/result.d.ts +20 -0
  52. package/dist/plugin/result.js +59 -0
  53. package/dist/plugin/result.js.map +1 -0
  54. package/dist/plugins/drive/chunker.d.ts +28 -0
  55. package/dist/plugins/drive/chunker.js +356 -0
  56. package/dist/plugins/drive/chunker.js.map +1 -0
  57. package/dist/plugins/drive/clean_text.d.ts +13 -0
  58. package/dist/plugins/drive/clean_text.js +31 -0
  59. package/dist/plugins/drive/clean_text.js.map +1 -0
  60. package/dist/plugins/drive/client.d.ts +25 -0
  61. package/dist/plugins/drive/client.js +62 -0
  62. package/dist/plugins/drive/client.js.map +1 -0
  63. package/dist/plugins/drive/embeddings.d.ts +15 -0
  64. package/dist/plugins/drive/embeddings.js +118 -0
  65. package/dist/plugins/drive/embeddings.js.map +1 -0
  66. package/dist/plugins/drive/exclude.d.ts +11 -0
  67. package/dist/plugins/drive/exclude.js +36 -0
  68. package/dist/plugins/drive/exclude.js.map +1 -0
  69. package/dist/plugins/drive/extract_text.d.ts +26 -0
  70. package/dist/plugins/drive/extract_text.js +108 -0
  71. package/dist/plugins/drive/extract_text.js.map +1 -0
  72. package/dist/plugins/drive/get_token.d.ts +5 -0
  73. package/dist/plugins/drive/get_token.js +113 -0
  74. package/dist/plugins/drive/get_token.js.map +1 -0
  75. package/dist/plugins/drive/index.d.ts +5 -0
  76. package/dist/plugins/drive/index.js +72 -0
  77. package/dist/plugins/drive/index.js.map +1 -0
  78. package/dist/plugins/drive/indexer.d.ts +69 -0
  79. package/dist/plugins/drive/indexer.js +204 -0
  80. package/dist/plugins/drive/indexer.js.map +1 -0
  81. package/dist/plugins/drive/mathpix.d.ts +4 -0
  82. package/dist/plugins/drive/mathpix.js +102 -0
  83. package/dist/plugins/drive/mathpix.js.map +1 -0
  84. package/dist/plugins/drive/path.d.ts +40 -0
  85. package/dist/plugins/drive/path.js +87 -0
  86. package/dist/plugins/drive/path.js.map +1 -0
  87. package/dist/plugins/drive/qdrant.d.ts +50 -0
  88. package/dist/plugins/drive/qdrant.js +61 -0
  89. package/dist/plugins/drive/qdrant.js.map +1 -0
  90. package/dist/plugins/drive/reindex.d.ts +2 -0
  91. package/dist/plugins/drive/reindex.js +263 -0
  92. package/dist/plugins/drive/reindex.js.map +1 -0
  93. package/dist/plugins/drive/settings.d.ts +11 -0
  94. package/dist/plugins/drive/settings.js +13 -0
  95. package/dist/plugins/drive/settings.js.map +1 -0
  96. package/dist/plugins/drive/sync.d.ts +16 -0
  97. package/dist/plugins/drive/sync.js +139 -0
  98. package/dist/plugins/drive/sync.js.map +1 -0
  99. package/dist/plugins/drive/sync_state.d.ts +3 -0
  100. package/dist/plugins/drive/sync_state.js +26 -0
  101. package/dist/plugins/drive/sync_state.js.map +1 -0
  102. package/dist/plugins/drive/tools/list_drive_folder.d.ts +104 -0
  103. package/dist/plugins/drive/tools/list_drive_folder.js +221 -0
  104. package/dist/plugins/drive/tools/list_drive_folder.js.map +1 -0
  105. package/dist/plugins/drive/tools/read_drive_file.d.ts +66 -0
  106. package/dist/plugins/drive/tools/read_drive_file.js +206 -0
  107. package/dist/plugins/drive/tools/read_drive_file.js.map +1 -0
  108. package/dist/plugins/drive/tools/search_drive.d.ts +52 -0
  109. package/dist/plugins/drive/tools/search_drive.js +78 -0
  110. package/dist/plugins/drive/tools/search_drive.js.map +1 -0
  111. package/dist/plugins/drive/tools/semantic_search_drive.d.ts +109 -0
  112. package/dist/plugins/drive/tools/semantic_search_drive.js +228 -0
  113. package/dist/plugins/drive/tools/semantic_search_drive.js.map +1 -0
  114. package/dist/plugins/github/index.d.ts +1 -0
  115. package/dist/plugins/github/index.js +27 -0
  116. package/dist/plugins/github/index.js.map +1 -0
  117. package/dist/plugins/github/repo_tree.d.ts +57 -0
  118. package/dist/plugins/github/repo_tree.js +123 -0
  119. package/dist/plugins/github/repo_tree.js.map +1 -0
  120. package/dist/plugins/index.d.ts +11 -0
  121. package/dist/plugins/index.js +7 -0
  122. package/dist/plugins/index.js.map +1 -0
  123. package/dist/plugins/railway/index.d.ts +1 -0
  124. package/dist/plugins/railway/index.js +21 -0
  125. package/dist/plugins/railway/index.js.map +1 -0
  126. package/dist/plugins/railway/tools.d.ts +114 -0
  127. package/dist/plugins/railway/tools.js +165 -0
  128. package/dist/plugins/railway/tools.js.map +1 -0
  129. package/dist/plugins/slack/api.d.ts +63 -0
  130. package/dist/plugins/slack/api.js +280 -0
  131. package/dist/plugins/slack/api.js.map +1 -0
  132. package/dist/plugins/slack/index.d.ts +1 -0
  133. package/dist/plugins/slack/index.js +80 -0
  134. package/dist/plugins/slack/index.js.map +1 -0
  135. package/dist/plugins/slack/tools/slack_files.d.ts +87 -0
  136. package/dist/plugins/slack/tools/slack_files.js +204 -0
  137. package/dist/plugins/slack/tools/slack_files.js.map +1 -0
  138. package/dist/plugins/slack/tools/slack_list_channels.d.ts +46 -0
  139. package/dist/plugins/slack/tools/slack_list_channels.js +197 -0
  140. package/dist/plugins/slack/tools/slack_list_channels.js.map +1 -0
  141. package/dist/plugins/slack/tools/slack_read_channel.d.ts +56 -0
  142. package/dist/plugins/slack/tools/slack_read_channel.js +150 -0
  143. package/dist/plugins/slack/tools/slack_read_channel.js.map +1 -0
  144. package/dist/plugins/slack/tools/slack_search.d.ts +53 -0
  145. package/dist/plugins/slack/tools/slack_search.js +132 -0
  146. package/dist/plugins/slack/tools/slack_search.js.map +1 -0
  147. package/dist/run.d.ts +8 -0
  148. package/dist/run.js +270 -0
  149. package/dist/run.js.map +1 -0
  150. package/dist/server/http.d.ts +5 -0
  151. package/dist/server/http.js +406 -0
  152. package/dist/server/http.js.map +1 -0
  153. package/dist/server/identity.d.ts +17 -0
  154. package/dist/server/identity.js +47 -0
  155. package/dist/server/identity.js.map +1 -0
  156. package/dist/server/mcp.d.ts +7 -0
  157. package/dist/server/mcp.js +55 -0
  158. package/dist/server/mcp.js.map +1 -0
  159. package/dist/server/stdio.d.ts +2 -0
  160. package/dist/server/stdio.js +40 -0
  161. package/dist/server/stdio.js.map +1 -0
  162. package/dist/store/index.d.ts +11 -0
  163. package/dist/store/index.js +31 -0
  164. package/dist/store/index.js.map +1 -0
  165. package/dist/store/keys.d.ts +9 -0
  166. package/dist/store/keys.js +32 -0
  167. package/dist/store/keys.js.map +1 -0
  168. package/dist/store/sqlite.d.ts +38 -0
  169. package/dist/store/sqlite.js +316 -0
  170. package/dist/store/sqlite.js.map +1 -0
  171. package/dist/store/supabase.d.ts +35 -0
  172. package/dist/store/supabase.js +269 -0
  173. package/dist/store/supabase.js.map +1 -0
  174. package/dist/store/supabase_client.d.ts +10 -0
  175. package/dist/store/supabase_client.js +31 -0
  176. package/dist/store/supabase_client.js.map +1 -0
  177. package/dist/store/tool_log.d.ts +12 -0
  178. package/dist/store/tool_log.js +8 -0
  179. package/dist/store/tool_log.js.map +1 -0
  180. package/dist/store/types.d.ts +96 -0
  181. package/dist/store/types.js +2 -0
  182. package/dist/store/types.js.map +1 -0
  183. package/dist/version.d.ts +1 -0
  184. package/dist/version.js +11 -0
  185. package/dist/version.js.map +1 -0
  186. package/package.json +84 -4
  187. package/sql/supabase.sql +185 -0
@@ -0,0 +1,54 @@
1
+ import { getConfig } from './config.js';
2
+ // 도구군 없이도 항상 허용하는 안전 도구(health check). 명시 화이트리스트.
3
+ const NO_SCOPE_TOOLS = new Set(['ping']);
4
+ /**
5
+ * 이 도구를 노출·호출해도 되는가. group = 도구가 속한 플러그인 id(registry.groupOf), 없으면 null.
6
+ * admin = 전체, member = allowed_tools(미설정=전체), viewer = config access.viewerTools 로 연 것만.
7
+ */
8
+ export function isToolAllowed(auth, name, group) {
9
+ if (auth.role === 'admin')
10
+ return true;
11
+ if (group === null)
12
+ return NO_SCOPE_TOOLS.has(name);
13
+ if (auth.role !== 'member')
14
+ return getConfig().access.viewerTools.includes(group);
15
+ const tools = auth.scopes?.tools;
16
+ if (!tools || tools.length === 0)
17
+ return true; // 미설정 = 모든 도구군
18
+ return tools.includes(group);
19
+ }
20
+ /** 허용 Slack 채널 id Set. null = 전체 허용(admin·미설정). */
21
+ export function allowedSlackChannels(auth) {
22
+ if (auth.role === 'admin')
23
+ return null;
24
+ const ch = auth.scopes?.slackChannels;
25
+ if (!ch || ch.length === 0)
26
+ return null;
27
+ return new Set(ch);
28
+ }
29
+ /** 허용 Drive 폴더 path prefix 배열. null = 전체 허용(admin·미설정). */
30
+ export function allowedDriveFolders(auth) {
31
+ if (auth.role === 'admin')
32
+ return null;
33
+ const f = auth.scopes?.driveFolders;
34
+ if (!f || f.length === 0)
35
+ return null;
36
+ return f;
37
+ }
38
+ /**
39
+ * path 가 허용 폴더 prefix 하위인가. prefixes null = 전체 허용.
40
+ * path 예: "개발문서/backend/..." · "01_고객사/Acme/제안서.pptx".
41
+ * prefix 예: "01_고객사" → 그 하위 전부 허용. 정확 일치 또는 "prefix/" 로 시작.
42
+ */
43
+ export function isPathInScope(path, prefixes) {
44
+ if (!prefixes || prefixes.length === 0)
45
+ return true;
46
+ const norm = String(path).replace(/^\/+/, '');
47
+ return prefixes.some((p) => {
48
+ const pp = String(p).replace(/^\/+|\/+$/g, '');
49
+ if (!pp)
50
+ return false;
51
+ return norm === pp || norm.startsWith(pp + '/');
52
+ });
53
+ }
54
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/core/scope.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,kDAAkD;AAClD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAS,CAAC,MAAM,CAAC,CAAC,CAAC;AAEjD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAiB,EAAE,IAAY,EAAE,KAAoB;IACjF,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClF,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;IACjC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,eAAe;IAC9D,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,oBAAoB,CAAC,IAAiB;IACpD,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC;IACtC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC;AACrB,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,mBAAmB,CAAC,IAAiB;IACnD,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;IACpC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,QAAyB;IACnE,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC9C,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QACzB,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QAC/C,IAAI,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC;QACtB,OAAO,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { UpstreamSpec } from '../plugin/define.js';
3
+ export declare function isExposed(spec: UpstreamSpec, remoteName: string): boolean;
4
+ export declare class UpstreamManager {
5
+ private upstreams;
6
+ constructor(entries: Array<{
7
+ spec: UpstreamSpec;
8
+ group: string;
9
+ }>);
10
+ get size(): number;
11
+ connectAll(): Promise<void>;
12
+ /** Tools of connected remotes, with their permission group. */
13
+ tools(): Array<{
14
+ tool: Tool;
15
+ group: string;
16
+ }>;
17
+ /** The remote that owns this name, if any. */
18
+ owner(toolName: string): {
19
+ group: string;
20
+ call: (args: Record<string, unknown>) => Promise<unknown>;
21
+ } | null;
22
+ /** Prefix match only — used to tell "blocked remote write tool" apart from "unknown tool". */
23
+ prefixOwner(toolName: string): string | null;
24
+ statuses(): {
25
+ name: string;
26
+ connected: boolean;
27
+ toolCount: number;
28
+ error: string | null;
29
+ }[];
30
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * MCP gateway — connects to remote MCP servers as a client and re-exposes their tools under a name prefix,
3
+ * so one connector gives the user both local plugin tools and remote ones.
4
+ *
5
+ * Design:
6
+ * - graceful (fail-open): a remote that fails to connect only drops its own tools; the server keeps running.
7
+ * - best-effort connect after listen + tools/list cache. Each MCP session reads the cache.
8
+ * - names: `{prefix}{remote name}` (e.g. github_search_code). The prefix routes calls back.
9
+ * - read-only by default: only tools whose first word is a read verb are listed and callable, so the
10
+ * shared server token can't be used to edit code, merge or delete through the gateway.
11
+ *
12
+ * Remotes come from plugins (`upstreams` in a plugin) and from the config file (`upstreams: [...]`).
13
+ */
14
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
16
+ import { VERSION } from '../version.js';
17
+ /**
18
+ * Allowlist (default-deny) of read verbs. A blocklist of write verbs used to let unknown verbs
19
+ * (set_, enable_, dispatch_, assign_, submit_ ...) through as "read-only"; now an unknown verb is blocked.
20
+ * A read tool blocked by a missing verb is a lost feature, but the safe side. Set `expose` to open more.
21
+ */
22
+ const READ_VERBS = new Set([
23
+ 'get', 'list', 'search', 'read', 'fetch', 'find', 'show', 'describe', 'view',
24
+ // 'resolve' is not here — it names state-changing tools such as resolving a review thread.
25
+ 'query', 'count', 'check', 'status', 'ping', 'download', 'export',
26
+ 'lookup', 'diff', 'blame', 'compare', 'log', 'history', 'scan', 'inspect',
27
+ ]);
28
+ /** First word of a remote tool name: get_file_contents → get, getFile → get, notion-search → notion. */
29
+ function firstWord(remoteName) {
30
+ return remoteName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').split(/[_\-.\s]/)[0].toLowerCase();
31
+ }
32
+ export function isExposed(spec, remoteName) {
33
+ const expose = spec.expose ?? 'read';
34
+ if (expose === 'all')
35
+ return true;
36
+ if (Array.isArray(expose))
37
+ return expose.includes(remoteName);
38
+ return READ_VERBS.has(firstWord(remoteName));
39
+ }
40
+ class Upstream {
41
+ spec;
42
+ group;
43
+ client = null;
44
+ transport = null;
45
+ tools = []; // prefixed, exposed only
46
+ connected = false;
47
+ lastError = null;
48
+ connecting = null; // single-flight: concurrent reconnects share one attempt
49
+ constructor(spec, group) {
50
+ this.spec = spec;
51
+ this.group = group;
52
+ }
53
+ /** best-effort connect + tools/list cache. Never throws. */
54
+ async connect() {
55
+ if (this.connecting)
56
+ return this.connecting;
57
+ this.connecting = this.doConnect().finally(() => {
58
+ this.connecting = null;
59
+ });
60
+ return this.connecting;
61
+ }
62
+ async doConnect() {
63
+ // Close the previous transport before replacing it so sockets and SSE streams don't leak.
64
+ const prev = this.transport;
65
+ this.client = null;
66
+ this.transport = null;
67
+ this.connected = false;
68
+ if (prev)
69
+ await prev.close().catch(() => { });
70
+ try {
71
+ const headers = { ...(this.spec.headers ?? {}) };
72
+ const token = typeof this.spec.token === 'function' ? this.spec.token() : this.spec.token;
73
+ if (token)
74
+ headers.Authorization = `Bearer ${token}`;
75
+ const transport = new StreamableHTTPClientTransport(new URL(this.spec.url), { requestInit: { headers } });
76
+ const client = new Client({ name: 'knowledge-mcp-gateway', version: VERSION }, { capabilities: {} });
77
+ await client.connect(transport);
78
+ const res = await client.listTools();
79
+ this.tools = res.tools
80
+ .filter((t) => isExposed(this.spec, t.name))
81
+ .map((t) => ({ ...t, name: `${this.spec.prefix}${t.name}`, description: `[${this.spec.name}] ${t.description ?? ''}` }));
82
+ // Commit only after success so a failed reconnect never exposes a half-built state.
83
+ this.transport = transport;
84
+ this.client = client;
85
+ this.connected = true;
86
+ this.lastError = null;
87
+ const hidden = res.tools.length - this.tools.length;
88
+ console.error(`[upstream:${this.spec.name}] connected · ${this.tools.length} tools (prefix ${this.spec.prefix})` +
89
+ (hidden ? ` · ${hidden} hidden (not read-only)` : ''));
90
+ }
91
+ catch (e) {
92
+ this.lastError = e instanceof Error ? e.message : String(e);
93
+ this.connected = false;
94
+ console.error(`[upstream:${this.spec.name}] connect failed (skipped): ${this.lastError}`);
95
+ }
96
+ }
97
+ getTools() {
98
+ return this.connected ? this.tools : [];
99
+ }
100
+ /** Whether this remote handles the name: prefix matches and the remote tool is exposed. */
101
+ claims(toolName) {
102
+ return toolName.startsWith(this.spec.prefix) && isExposed(this.spec, toolName.slice(this.spec.prefix.length));
103
+ }
104
+ /** Strip the prefix and forward. Reconnects once if the link dropped. */
105
+ async callTool(toolName, args) {
106
+ const remoteName = toolName.slice(this.spec.prefix.length);
107
+ if (!this.client || !this.connected) {
108
+ await this.connect();
109
+ if (!this.client || !this.connected) {
110
+ throw new Error(`[upstream:${this.spec.name}] not connected${this.lastError ? ` (${this.lastError})` : ''}`);
111
+ }
112
+ }
113
+ try {
114
+ return await this.client.callTool({ name: remoteName, arguments: args });
115
+ }
116
+ catch (e) {
117
+ this.connected = false;
118
+ await this.connect();
119
+ if (!this.client || !this.connected)
120
+ throw e;
121
+ return await this.client.callTool({ name: remoteName, arguments: args });
122
+ }
123
+ }
124
+ status() {
125
+ return { name: this.spec.name, connected: this.connected, toolCount: this.tools.length, error: this.lastError };
126
+ }
127
+ }
128
+ export class UpstreamManager {
129
+ upstreams = [];
130
+ constructor(entries) {
131
+ const prefixes = new Set();
132
+ for (const { spec, group } of entries) {
133
+ if (!spec.prefix)
134
+ throw new Error(`[upstream:${spec.name}] prefix is required`);
135
+ if (prefixes.has(spec.prefix))
136
+ throw new Error(`[upstream] two remotes share the prefix "${spec.prefix}"`);
137
+ prefixes.add(spec.prefix);
138
+ this.upstreams.push(new Upstream(spec, group));
139
+ }
140
+ }
141
+ get size() {
142
+ return this.upstreams.length;
143
+ }
144
+ async connectAll() {
145
+ await Promise.all(this.upstreams.map((u) => u.connect()));
146
+ }
147
+ /** Tools of connected remotes, with their permission group. */
148
+ tools() {
149
+ return this.upstreams.flatMap((u) => u.getTools().map((tool) => ({ tool, group: u.group })));
150
+ }
151
+ /** The remote that owns this name, if any. */
152
+ owner(toolName) {
153
+ const u = this.upstreams.find((x) => x.claims(toolName));
154
+ return u ? { group: u.group, call: (args) => u.callTool(toolName, args) } : null;
155
+ }
156
+ /** Prefix match only — used to tell "blocked remote write tool" apart from "unknown tool". */
157
+ prefixOwner(toolName) {
158
+ return this.upstreams.find((u) => toolName.startsWith(u.spec.prefix))?.spec.name ?? null;
159
+ }
160
+ statuses() {
161
+ return this.upstreams.map((u) => u.status());
162
+ }
163
+ }
164
+ //# sourceMappingURL=upstream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream.js","sourceRoot":"","sources":["../../src/core/upstream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAGnG,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC;;;;GAIG;AACH,MAAM,UAAU,GAAG,IAAI,GAAG,CAAS;IACjC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IAC5E,2FAA2F;IAC3F,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ;IACjE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS;CAC1E,CAAC,CAAC;AAEH,wGAAwG;AACxG,SAAS,SAAS,CAAC,UAAkB;IACnC,OAAO,UAAU,CAAC,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAC;AAC/F,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAkB,EAAE,UAAkB;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC;IACrC,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC9D,OAAO,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,QAAQ;IASM;IACA;IATV,MAAM,GAAkB,IAAI,CAAC;IAC7B,SAAS,GAAyC,IAAI,CAAC;IACvD,KAAK,GAAW,EAAE,CAAC,CAAC,yBAAyB;IAC7C,SAAS,GAAG,KAAK,CAAC;IAClB,SAAS,GAAkB,IAAI,CAAC;IAChC,UAAU,GAAyB,IAAI,CAAC,CAAC,yDAAyD;IAE1G,YACkB,IAAkB,EAClB,KAAa;QADb,SAAI,GAAJ,IAAI,CAAc;QAClB,UAAK,GAAL,KAAK,CAAQ;IAC5B,CAAC;IAEJ,4DAA4D;IAC5D,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO,IAAI,CAAC,UAAU,CAAC;QAC5C,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAC9C,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAEO,KAAK,CAAC,SAAS;QACrB,0FAA0F;QAC1F,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,IAAI,IAAI;YAAE,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAE7C,IAAI,CAAC;YACH,MAAM,OAAO,GAA2B,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,CAAC;YACzE,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;YAC1F,IAAI,KAAK;gBAAE,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAC;YAErD,MAAM,SAAS,GAAG,IAAI,6BAA6B,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;YAC1G,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;YACrG,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAChC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;YAErC,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK;iBACnB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;iBAC3C,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,WAAW,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;YAC3H,oFAAoF;YACpF,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;YAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACrB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;YACpD,OAAO,CAAC,KAAK,CACX,aAAa,IAAI,CAAC,IAAI,CAAC,IAAI,iBAAiB,IAAI,CAAC,KAAK,CAAC,MAAM,kBAAkB,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG;gBAChG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,yBAAyB,CAAC,CAAC,CAAC,EAAE,CAAC,CACxD,CAAC;QACJ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,SAAS,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC5D,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACvB,OAAO,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,IAAI,CAAC,IAAI,+BAA+B,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1C,CAAC;IAED,2FAA2F;IAC3F,MAAM,CAAC,QAAgB;QACrB,OAAO,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChH,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,QAAQ,CAAC,QAAgB,EAAE,IAA6B;QAC5D,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3D,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpC,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACpC,MAAM,IAAI,KAAK,CAAC,aAAa,IAAI,CAAC,IAAI,CAAC,IAAI,kBAAkB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC/G,CAAC;QACH,CAAC;QACD,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACvB,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS;gBAAE,MAAM,CAAC,CAAC;YAC7C,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;IAClH,CAAC;CACF;AAED,MAAM,OAAO,eAAe;IAClB,SAAS,GAAe,EAAE,CAAC;IAEnC,YAAY,OAAqD;QAC/D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QACnC,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,OAAO,EAAE,CAAC;YACtC,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,aAAa,IAAI,CAAC,IAAI,sBAAsB,CAAC,CAAC;YAChF,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;YAC3G,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,+DAA+D;IAC/D,KAAK;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/F,CAAC;IAED,8CAA8C;IAC9C,KAAK,CAAC,QAAgB;QACpB,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QACzD,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACnF,CAAC;IAED,8FAA8F;IAC9F,WAAW,CAAC,QAAgB;QAC1B,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC;IAC3F,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/C,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @chnxthr/knowledge-mcp — build your own MCP server from plugins.
3
+ *
4
+ * import { run, builtinPlugins, loadPlugins } from '@chnxthr/knowledge-mcp';
5
+ * await run({ plugins: [...builtinPlugins, ...(await loadPlugins(new URL('./plugins/', import.meta.url)))] });
6
+ *
7
+ * Importing this module loads .env and installs log masking first (bootstrap.ts), so import it before
8
+ * anything that reads process.env.
9
+ */
10
+ import './bootstrap.js';
11
+ export { definePlugin, defineTool, ToolError, isPlugin } from './plugin/define.js';
12
+ export type { Plugin, PluginSpec, PluginCommand, PluginState, Tool, ToolSpec, ToolContext, ToolResult, ToolReturn, UpstreamSpec, } from './plugin/define.js';
13
+ export { loadPlugins } from './plugin/load.js';
14
+ export { Registry } from './plugin/registry.js';
15
+ export { run, type RunOptions } from './run.js';
16
+ export { builtinPlugins, drivePlugin, slackPlugin, githubPlugin, railwayPlugin } from './plugins/index.js';
17
+ export { getAuth, getAuthMaybe } from './core/context.js';
18
+ export { getConfig } from './core/config.js';
19
+ export { getStore } from './store/index.js';
20
+ export type { Store } from './store/types.js';
21
+ export type { AuthContext, Role, ResourceScopes } from './core/auth.js';
22
+ export { VERSION } from './version.js';
23
+ export { z } from 'zod';
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @chnxthr/knowledge-mcp — build your own MCP server from plugins.
3
+ *
4
+ * import { run, builtinPlugins, loadPlugins } from '@chnxthr/knowledge-mcp';
5
+ * await run({ plugins: [...builtinPlugins, ...(await loadPlugins(new URL('./plugins/', import.meta.url)))] });
6
+ *
7
+ * Importing this module loads .env and installs log masking first (bootstrap.ts), so import it before
8
+ * anything that reads process.env.
9
+ */
10
+ import './bootstrap.js';
11
+ export { definePlugin, defineTool, ToolError, isPlugin } from './plugin/define.js';
12
+ export { loadPlugins } from './plugin/load.js';
13
+ export { Registry } from './plugin/registry.js';
14
+ export { run } from './run.js';
15
+ export { builtinPlugins, drivePlugin, slackPlugin, githubPlugin, railwayPlugin } from './plugins/index.js';
16
+ export { getAuth, getAuthMaybe } from './core/context.js';
17
+ export { getConfig } from './core/config.js';
18
+ export { getStore } from './store/index.js';
19
+ export { VERSION } from './version.js';
20
+ export { z } from 'zod';
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,gBAAgB,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAInF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,GAAG,EAAmB,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC3G,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Plugin API — the only thing you need to add tools to Knowledge MCP.
3
+ *
4
+ * A plugin is one connector (Drive, Slack, your internal API, ...). Its `id` is also its permission group:
5
+ * admins see every plugin, members see the groups in their allowed_tools (all when unset), and viewers see
6
+ * only the groups listed in config `access.viewerTools`.
7
+ *
8
+ * export default definePlugin({
9
+ * id: 'weather',
10
+ * env: ['WEATHER_API_KEY'], // tools stay hidden until every name here is set
11
+ * tools: [
12
+ * defineTool({
13
+ * name: 'weather_now',
14
+ * description: 'Current weather for a city',
15
+ * input: z.object({ city: z.string().describe('City name') }),
16
+ * run: async ({ city }) => fetchWeather(city), // return a string, any JSON value, or an MCP result
17
+ * }),
18
+ * ],
19
+ * });
20
+ *
21
+ * The core lists the tools, validates input against the zod schema (zod 3 or 4), checks the caller's
22
+ * permission, logs the call, and turns return values and thrown errors into MCP results.
23
+ */
24
+ import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
25
+ import type { AnySchema, SchemaOutput } from '@modelcontextprotocol/sdk/server/zod-compat.js';
26
+ import type { AuthContext, Role } from '../core/auth.js';
27
+ export declare const PLUGIN_BRAND: unique symbol;
28
+ declare const TOOL_BRAND: unique symbol;
29
+ /** MCP tool result. Returned as is when a tool returns an object with a `content` array. */
30
+ export type ToolResult = {
31
+ content: Array<{
32
+ type: string;
33
+ [k: string]: unknown;
34
+ }>;
35
+ isError?: boolean;
36
+ [k: string]: unknown;
37
+ };
38
+ /** What `run` may return: text, any JSON-serializable value (sent as pretty JSON), or a full MCP result. */
39
+ export type ToolReturn = string | ToolResult | Record<string, unknown> | unknown[] | number | boolean | null | undefined;
40
+ /** Small key-value storage, kept per plugin in the server's store (SQLite file or Supabase). */
41
+ export interface PluginState {
42
+ get<T = unknown>(key: string): Promise<T | null>;
43
+ set(key: string, value: unknown): Promise<void>;
44
+ delete(key: string): Promise<void>;
45
+ }
46
+ export interface ToolContext {
47
+ /** The caller. `role` is admin, member or viewer. */
48
+ user: {
49
+ email?: string;
50
+ role: Role;
51
+ keyId?: string;
52
+ };
53
+ /** Full auth context (scopes for Slack channels and Drive folders, act-as flag). */
54
+ auth: AuthContext;
55
+ /** This plugin's section of the config file (`plugins.<id>`), validated by `configSchema` when given. */
56
+ config: unknown;
57
+ /** Persistent key-value storage for this plugin. */
58
+ state: PluginState;
59
+ /** Log to stderr with the plugin prefix. Secrets are masked. Never use console.log in stdio mode. */
60
+ log: (...args: unknown[]) => void;
61
+ }
62
+ export interface ToolSpec<S extends AnySchema = AnySchema> {
63
+ /** Tool name shown to the model. Lowercase letters, digits and underscores; unique across all plugins. */
64
+ name: string;
65
+ /** What the tool does and when to use it. The model reads this to pick the tool. */
66
+ description: string;
67
+ /** zod object schema for the arguments. Describe each field with `.describe()`. Omit for no arguments. */
68
+ input?: S;
69
+ /** MCP tool hints (readOnlyHint, destructiveHint, ...). */
70
+ annotations?: ToolAnnotations;
71
+ /** Extra condition on top of the plugin's. Return true, or a short reason why the tool is off. */
72
+ enabled?: () => boolean | string;
73
+ run: (input: SchemaOutput<S>, ctx: ToolContext) => Promise<ToolReturn> | ToolReturn;
74
+ }
75
+ /** A remote MCP server whose tools are re-exposed under a name prefix (the server is a gateway). */
76
+ export interface UpstreamSpec {
77
+ /** Name for logs. */
78
+ name: string;
79
+ /** Streamable HTTP endpoint of the remote MCP server. */
80
+ url: string;
81
+ /** Prefix added to every remote tool name, e.g. "github_" turns get_me into github_get_me. */
82
+ prefix: string;
83
+ /** Bearer token sent to the remote server. A function is read at connect time. */
84
+ token?: string | (() => string | undefined);
85
+ headers?: Record<string, string>;
86
+ /**
87
+ * Which remote tools to expose. "read" (default) keeps tools whose first word is a read verb
88
+ * (get, list, search, ...) and blocks the rest. "all" exposes everything. A list exposes exactly
89
+ * those remote names (without the prefix).
90
+ */
91
+ expose?: 'read' | 'all' | string[];
92
+ }
93
+ /** A CLI subcommand: `knowledge-mcp <plugin id> <command> [args]`. */
94
+ export interface PluginCommand {
95
+ description: string;
96
+ run: (args: string[]) => Promise<void> | void;
97
+ }
98
+ export interface PluginSpec {
99
+ /** Permission group and config key. Lowercase letters, digits and dashes. */
100
+ id: string;
101
+ /** One line shown by `knowledge-mcp doctor`. */
102
+ description?: string;
103
+ /** Env vars that must all be set (non-empty) for the plugin to turn on. */
104
+ env?: string[];
105
+ /** Extra condition, checked after `env`. Return true, or a short reason why the plugin is off. */
106
+ enabled?: () => boolean | string;
107
+ tools?: ToolSpec<any>[];
108
+ /** Remote MCP servers to proxy. Their tools join this plugin's permission group. */
109
+ upstreams?: UpstreamSpec[] | (() => UpstreamSpec[]);
110
+ /** Usage notes appended to the server instructions the client receives. */
111
+ instructions?: string;
112
+ /** zod schema for `plugins.<id>` in the config file. */
113
+ configSchema?: AnySchema;
114
+ /**
115
+ * Runs once after the HTTP server is listening (background jobs, warm-up). Errors are logged, not fatal.
116
+ * Not run in stdio mode, where each client session is its own process.
117
+ */
118
+ start?: (ctx: {
119
+ config: unknown;
120
+ state: PluginState;
121
+ log: (...args: unknown[]) => void;
122
+ }) => Promise<void> | void;
123
+ /** Extra CLI commands. */
124
+ commands?: Record<string, PluginCommand>;
125
+ }
126
+ export type Plugin = PluginSpec & {
127
+ readonly [PLUGIN_BRAND]: true;
128
+ };
129
+ export type Tool<S extends AnySchema = AnySchema> = ToolSpec<S> & {
130
+ readonly [TOOL_BRAND]: true;
131
+ };
132
+ /** Define a tool. Gives `run` a typed `input` from the zod schema. */
133
+ export declare function defineTool<S extends AnySchema = AnySchema>(spec: ToolSpec<S>): Tool<S>;
134
+ /** Define a plugin. Validates the shape early so mistakes fail at boot with a clear message. */
135
+ export declare function definePlugin(spec: PluginSpec): Plugin;
136
+ export declare function isPlugin(v: unknown): v is Plugin;
137
+ /**
138
+ * Throw this from `run` for an expected failure (not found, bad state, missing permission).
139
+ * The model receives `[error] <message>` and the call is logged as failed.
140
+ */
141
+ export declare class ToolError extends Error {
142
+ constructor(message: string);
143
+ }
144
+ export {};
@@ -0,0 +1,44 @@
1
+ export const PLUGIN_BRAND = Symbol.for('knowledge-mcp.plugin');
2
+ const TOOL_BRAND = Symbol.for('knowledge-mcp.tool');
3
+ const TOOL_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
4
+ const PLUGIN_ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
5
+ /** Define a tool. Gives `run` a typed `input` from the zod schema. */
6
+ export function defineTool(spec) {
7
+ if (!TOOL_NAME_RE.test(spec.name)) {
8
+ throw new Error(`[plugin] invalid tool name "${spec.name}": use lowercase letters, digits and underscores (max 64)`);
9
+ }
10
+ if (!spec.description?.trim())
11
+ throw new Error(`[plugin] tool "${spec.name}" needs a description`);
12
+ if (typeof spec.run !== 'function')
13
+ throw new Error(`[plugin] tool "${spec.name}" needs a run function`);
14
+ return Object.assign({}, spec, { [TOOL_BRAND]: true });
15
+ }
16
+ /** Define a plugin. Validates the shape early so mistakes fail at boot with a clear message. */
17
+ export function definePlugin(spec) {
18
+ if (!PLUGIN_ID_RE.test(spec.id)) {
19
+ throw new Error(`[plugin] invalid plugin id "${spec.id}": use lowercase letters, digits and dashes (max 32)`);
20
+ }
21
+ const names = new Set();
22
+ for (const t of spec.tools ?? []) {
23
+ if (!t[TOOL_BRAND])
24
+ defineTool(t); // validates plain objects too
25
+ if (names.has(t.name))
26
+ throw new Error(`[plugin] ${spec.id}: duplicate tool name "${t.name}"`);
27
+ names.add(t.name);
28
+ }
29
+ return Object.assign({}, spec, { [PLUGIN_BRAND]: true });
30
+ }
31
+ export function isPlugin(v) {
32
+ return !!v && typeof v === 'object' && v[PLUGIN_BRAND] === true;
33
+ }
34
+ /**
35
+ * Throw this from `run` for an expected failure (not found, bad state, missing permission).
36
+ * The model receives `[error] <message>` and the call is logged as failed.
37
+ */
38
+ export class ToolError extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = 'ToolError';
42
+ }
43
+ }
44
+ //# sourceMappingURL=define.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define.js","sourceRoot":"","sources":["../../src/plugin/define.ts"],"names":[],"mappings":"AA2BA,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;AAC/D,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AAmGpD,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAC9C,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAE9C,sEAAsE;AACtE,MAAM,UAAU,UAAU,CAAkC,IAAiB;IAC3E,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,+BAA+B,IAAI,CAAC,IAAI,2DAA2D,CAAC,CAAC;IACvH,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,CAAC,IAAI,uBAAuB,CAAC,CAAC;IACnG,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,CAAC,IAAI,wBAAwB,CAAC,CAAC;IACzG,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,IAAa,EAAE,CAAC,CAAC;AAClE,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,YAAY,CAAC,IAAgB;IAC3C,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,+BAA+B,IAAI,CAAC,EAAE,sDAAsD,CAAC,CAAC;IAChH,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;QACjC,IAAI,CAAE,CAAmB,CAAC,UAAU,CAAC;YAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,8BAA8B;QACpF,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,CAAC,EAAE,0BAA0B,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QAC/F,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,IAAa,EAAE,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,CAAU;IACjC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAK,CAA6B,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;AAC/F,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAClC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ import { type Plugin } from './define.js';
2
+ export declare function loadPlugins(dir: string | URL): Promise<Plugin[]>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * loadPlugins(dir) — imports every plugin file in a folder, so adding a tool is adding a file.
3
+ *
4
+ * await run({ plugins: [...builtinPlugins, ...(await loadPlugins(new URL('./plugins/', import.meta.url)))] });
5
+ *
6
+ * Loads *.ts / *.js / *.mts / *.mjs in name order. Skips *.d.ts, *.test.*, *.spec.* and names starting
7
+ * with "_" or ".". Each file's default export must be a plugin made with definePlugin.
8
+ * Works under tsx (src/*.ts) and after tsc (dist/*.js) with the same import.meta.url-relative path.
9
+ */
10
+ import { existsSync, readdirSync, statSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { fileURLToPath, pathToFileURL } from 'node:url';
13
+ import { isPlugin } from './define.js';
14
+ const PLUGIN_FILE = /\.(m?[jt]s)$/;
15
+ const SKIP = /(\.d\.[mc]?ts$)|(\.(test|spec)\.[mc]?[jt]s$)|(\.map$)/;
16
+ export async function loadPlugins(dir) {
17
+ const path = dir instanceof URL ? fileURLToPath(dir) : dir;
18
+ if (!existsSync(path))
19
+ return [];
20
+ const files = readdirSync(path)
21
+ .filter((f) => PLUGIN_FILE.test(f) && !SKIP.test(f) && !f.startsWith('_') && !f.startsWith('.'))
22
+ .filter((f) => statSync(join(path, f)).isFile())
23
+ .sort();
24
+ const out = [];
25
+ for (const f of files) {
26
+ const full = join(path, f);
27
+ let mod;
28
+ try {
29
+ mod = (await import(pathToFileURL(full).href));
30
+ }
31
+ catch (e) {
32
+ throw new Error(`[plugin] failed to load ${full}: ${e instanceof Error ? e.message : String(e)}`);
33
+ }
34
+ if (!isPlugin(mod.default)) {
35
+ throw new Error(`[plugin] ${full}: the default export must be definePlugin({...})`);
36
+ }
37
+ out.push(mod.default);
38
+ }
39
+ return out;
40
+ }
41
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/plugin/load.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAe,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,GAAG,cAAc,CAAC;AACnC,MAAM,IAAI,GAAG,uDAAuD,CAAC;AAErE,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAiB;IACjD,MAAM,IAAI,GAAG,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC;SAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;SAC/F,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SAC/C,IAAI,EAAE,CAAC;IACV,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC3B,IAAI,GAA0B,CAAC;QAC/B,IAAI,CAAC;YACH,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAA0B,CAAC;QAC1E,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACpG,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,kDAAkD,CAAC,CAAC;QACtF,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Health check tool — always listed, no permission group. Shows who you are and which plugins are on.
3
+ * Call it first when a tool you just added does not show up.
4
+ */
5
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
6
+ import type { AuthContext } from '../core/auth.js';
7
+ import type { Registry } from './registry.js';
8
+ export declare const pingTool: Tool;
9
+ export declare function handlePing(registry: Registry, auth: AuthContext, args: unknown): import("./define.js").ToolResult;
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ import { VERSION } from '../version.js';
3
+ import { inputJsonSchema, textResult } from './result.js';
4
+ const input = z.object({
5
+ echo: z.string().default('pong').describe('Returned as is. Default "pong".'),
6
+ });
7
+ export const pingTool = {
8
+ name: 'ping',
9
+ description: 'Health check. Verifies the server, your auth and which plugins are on. ' +
10
+ 'Call it first when a tool you expect is missing.',
11
+ inputSchema: inputJsonSchema('ping', input),
12
+ };
13
+ export function handlePing(registry, auth, args) {
14
+ const { echo } = input.parse(args ?? {});
15
+ const admin = auth.role === 'admin';
16
+ return textResult(JSON.stringify({
17
+ ok: true,
18
+ echo,
19
+ server: 'knowledge-mcp',
20
+ version: VERSION,
21
+ timestamp: new Date().toISOString(),
22
+ authenticated_as: auth.email ?? auth.org_id ?? null,
23
+ role: auth.role ?? null,
24
+ // Plugin on/off is useful to everyone; the reason (env names) only to admins.
25
+ plugins: registry.status().map((p) => (admin ? { id: p.id, enabled: p.enabled, reason: p.reason } : { id: p.id, enabled: p.enabled })),
26
+ upstreams: admin ? registry.upstreams.statuses() : undefined,
27
+ }, null, 2));
28
+ }
29
+ //# sourceMappingURL=ping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ping.js","sourceRoot":"","sources":["../../src/plugin/ping.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,iCAAiC,CAAC;CAC7E,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAS;IAC5B,IAAI,EAAE,MAAM;IACZ,WAAW,EACT,yEAAyE;QACzE,kDAAkD;IACpD,WAAW,EAAE,eAAe,CAAC,MAAM,EAAE,KAAK,CAAwB;CACnE,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,QAAkB,EAAE,IAAiB,EAAE,IAAa;IAC7E,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC;IACpC,OAAO,UAAU,CACf,IAAI,CAAC,SAAS,CACZ;QACE,EAAE,EAAE,IAAI;QACR,IAAI;QACJ,MAAM,EAAE,eAAe;QACvB,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,gBAAgB,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI;QACnD,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;QACvB,8EAA8E;QAC9E,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACtI,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS;KAC7D,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;AACJ,CAAC"}