@chnxthr/knowledge-mcp 0.0.0-stage → 0.3.1

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 (190) 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 +94 -0
  61. package/dist/plugins/drive/client.js +220 -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 +95 -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 +128 -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/ooxml.d.ts +17 -0
  85. package/dist/plugins/drive/ooxml.js +152 -0
  86. package/dist/plugins/drive/ooxml.js.map +1 -0
  87. package/dist/plugins/drive/path.d.ts +40 -0
  88. package/dist/plugins/drive/path.js +87 -0
  89. package/dist/plugins/drive/path.js.map +1 -0
  90. package/dist/plugins/drive/qdrant.d.ts +123 -0
  91. package/dist/plugins/drive/qdrant.js +148 -0
  92. package/dist/plugins/drive/qdrant.js.map +1 -0
  93. package/dist/plugins/drive/reindex.d.ts +2 -0
  94. package/dist/plugins/drive/reindex.js +263 -0
  95. package/dist/plugins/drive/reindex.js.map +1 -0
  96. package/dist/plugins/drive/settings.d.ts +11 -0
  97. package/dist/plugins/drive/settings.js +13 -0
  98. package/dist/plugins/drive/settings.js.map +1 -0
  99. package/dist/plugins/drive/sync.d.ts +16 -0
  100. package/dist/plugins/drive/sync.js +139 -0
  101. package/dist/plugins/drive/sync.js.map +1 -0
  102. package/dist/plugins/drive/sync_state.d.ts +3 -0
  103. package/dist/plugins/drive/sync_state.js +26 -0
  104. package/dist/plugins/drive/sync_state.js.map +1 -0
  105. package/dist/plugins/drive/tools/list_drive_folder.d.ts +104 -0
  106. package/dist/plugins/drive/tools/list_drive_folder.js +221 -0
  107. package/dist/plugins/drive/tools/list_drive_folder.js.map +1 -0
  108. package/dist/plugins/drive/tools/read_drive_file.d.ts +66 -0
  109. package/dist/plugins/drive/tools/read_drive_file.js +206 -0
  110. package/dist/plugins/drive/tools/read_drive_file.js.map +1 -0
  111. package/dist/plugins/drive/tools/search_drive.d.ts +52 -0
  112. package/dist/plugins/drive/tools/search_drive.js +78 -0
  113. package/dist/plugins/drive/tools/search_drive.js.map +1 -0
  114. package/dist/plugins/drive/tools/semantic_search_drive.d.ts +109 -0
  115. package/dist/plugins/drive/tools/semantic_search_drive.js +228 -0
  116. package/dist/plugins/drive/tools/semantic_search_drive.js.map +1 -0
  117. package/dist/plugins/github/index.d.ts +1 -0
  118. package/dist/plugins/github/index.js +27 -0
  119. package/dist/plugins/github/index.js.map +1 -0
  120. package/dist/plugins/github/repo_tree.d.ts +57 -0
  121. package/dist/plugins/github/repo_tree.js +123 -0
  122. package/dist/plugins/github/repo_tree.js.map +1 -0
  123. package/dist/plugins/index.d.ts +11 -0
  124. package/dist/plugins/index.js +7 -0
  125. package/dist/plugins/index.js.map +1 -0
  126. package/dist/plugins/railway/index.d.ts +1 -0
  127. package/dist/plugins/railway/index.js +21 -0
  128. package/dist/plugins/railway/index.js.map +1 -0
  129. package/dist/plugins/railway/tools.d.ts +114 -0
  130. package/dist/plugins/railway/tools.js +165 -0
  131. package/dist/plugins/railway/tools.js.map +1 -0
  132. package/dist/plugins/slack/api.d.ts +63 -0
  133. package/dist/plugins/slack/api.js +280 -0
  134. package/dist/plugins/slack/api.js.map +1 -0
  135. package/dist/plugins/slack/index.d.ts +1 -0
  136. package/dist/plugins/slack/index.js +80 -0
  137. package/dist/plugins/slack/index.js.map +1 -0
  138. package/dist/plugins/slack/tools/slack_files.d.ts +87 -0
  139. package/dist/plugins/slack/tools/slack_files.js +204 -0
  140. package/dist/plugins/slack/tools/slack_files.js.map +1 -0
  141. package/dist/plugins/slack/tools/slack_list_channels.d.ts +46 -0
  142. package/dist/plugins/slack/tools/slack_list_channels.js +197 -0
  143. package/dist/plugins/slack/tools/slack_list_channels.js.map +1 -0
  144. package/dist/plugins/slack/tools/slack_read_channel.d.ts +56 -0
  145. package/dist/plugins/slack/tools/slack_read_channel.js +150 -0
  146. package/dist/plugins/slack/tools/slack_read_channel.js.map +1 -0
  147. package/dist/plugins/slack/tools/slack_search.d.ts +53 -0
  148. package/dist/plugins/slack/tools/slack_search.js +132 -0
  149. package/dist/plugins/slack/tools/slack_search.js.map +1 -0
  150. package/dist/run.d.ts +8 -0
  151. package/dist/run.js +270 -0
  152. package/dist/run.js.map +1 -0
  153. package/dist/server/http.d.ts +5 -0
  154. package/dist/server/http.js +406 -0
  155. package/dist/server/http.js.map +1 -0
  156. package/dist/server/identity.d.ts +17 -0
  157. package/dist/server/identity.js +47 -0
  158. package/dist/server/identity.js.map +1 -0
  159. package/dist/server/mcp.d.ts +7 -0
  160. package/dist/server/mcp.js +55 -0
  161. package/dist/server/mcp.js.map +1 -0
  162. package/dist/server/stdio.d.ts +2 -0
  163. package/dist/server/stdio.js +40 -0
  164. package/dist/server/stdio.js.map +1 -0
  165. package/dist/store/index.d.ts +11 -0
  166. package/dist/store/index.js +31 -0
  167. package/dist/store/index.js.map +1 -0
  168. package/dist/store/keys.d.ts +9 -0
  169. package/dist/store/keys.js +32 -0
  170. package/dist/store/keys.js.map +1 -0
  171. package/dist/store/sqlite.d.ts +38 -0
  172. package/dist/store/sqlite.js +316 -0
  173. package/dist/store/sqlite.js.map +1 -0
  174. package/dist/store/supabase.d.ts +35 -0
  175. package/dist/store/supabase.js +269 -0
  176. package/dist/store/supabase.js.map +1 -0
  177. package/dist/store/supabase_client.d.ts +10 -0
  178. package/dist/store/supabase_client.js +31 -0
  179. package/dist/store/supabase_client.js.map +1 -0
  180. package/dist/store/tool_log.d.ts +12 -0
  181. package/dist/store/tool_log.js +8 -0
  182. package/dist/store/tool_log.js.map +1 -0
  183. package/dist/store/types.d.ts +96 -0
  184. package/dist/store/types.js +2 -0
  185. package/dist/store/types.js.map +1 -0
  186. package/dist/version.d.ts +1 -0
  187. package/dist/version.js +11 -0
  188. package/dist/version.js.map +1 -0
  189. package/package.json +80 -4
  190. package/sql/supabase.sql +185 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chanthr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,38 @@
1
- # Temporary Holding Version
1
+ # @chnxthr/knowledge-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Build your team's own MCP server from plugins. API keys, Google sign-in (OAuth 2.1), per-person permissions,
4
+ an audit log and a SQLite or Supabase store are built in, and so are Google Drive, Slack, GitHub and Railway.
5
+
6
+ ```bash
7
+ npx create-knowledge-mcp my-mcp # a ready project with an example plugin
8
+ ```
9
+
10
+ ```ts
11
+ // src/plugins/weather.ts — every file in src/plugins is a plugin
12
+ import { definePlugin, defineTool, z } from '@chnxthr/knowledge-mcp';
13
+
14
+ export default definePlugin({
15
+ id: 'weather',
16
+ env: ['WEATHER_API_KEY'],
17
+ tools: [
18
+ defineTool({
19
+ name: 'weather_now',
20
+ description: 'Current weather for a city',
21
+ input: z.object({ city: z.string().describe('City name') }),
22
+ run: async ({ city }) => (await fetch(`https://api.example.com/weather?q=${city}`)).json(),
23
+ }),
24
+ ],
25
+ });
26
+ ```
27
+
28
+ Run it without a project too:
29
+
30
+ ```bash
31
+ npx @chnxthr/knowledge-mcp doctor # which built-in connectors are on
32
+ npx @chnxthr/knowledge-mcp keys create --email you@example.com --role admin
33
+ npx @chnxthr/knowledge-mcp serve # http://localhost:8080/mcp
34
+ ```
35
+
36
+ Requires Node 22.13 or later. Documentation: https://github.com/chanthr/internal-mcp
37
+
38
+ MIT © chanthr
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Side effects that must run before any other module of the package is evaluated.
3
+ * index.ts imports this first: ESM evaluates imports in order, and several modules read process.env
4
+ * when they load, so .env has to be in place before them.
5
+ *
6
+ * 1. Load .env — MCP_ENV_FILE if set, else ./.env and <app root>/.env. Existing env vars win.
7
+ * 2. Mask secrets in every console.* line.
8
+ */
9
+ import { existsSync } from 'node:fs';
10
+ import { config as loadDotenv } from 'dotenv';
11
+ import { candidates } from './core/paths.js';
12
+ import { installLogMasking } from './core/log_mask.js';
13
+ const explicit = process.env.MCP_ENV_FILE;
14
+ const files = explicit ? [explicit] : candidates('.env').filter((f) => existsSync(f));
15
+ for (const path of files)
16
+ loadDotenv({ path, quiet: true });
17
+ installLogMasking();
18
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AACtF,KAAK,MAAM,IAAI,IAAI,KAAK;IAAE,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAE5D,iBAAiB,EAAE,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /** `knowledge-mcp` binary — the server with the built-in plugins (Drive, Slack, GitHub, Railway). */
3
+ import { run } from './index.js';
4
+ await run();
5
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,qGAAqG;AACrG,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEjC,MAAM,GAAG,EAAE,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * admin = every tool.
3
+ * member = a registered user (app_users row); limited by allowed_tools when set. Called "engineer" before 0.3.
4
+ * viewer = signed in but not registered; only `ping` and the groups in config access.viewerTools.
5
+ */
6
+ export type Role = 'admin' | 'member' | 'viewer';
7
+ /** 상수시간 문자열 비교 — sha256 으로 길이 정규화 후 timingSafeEqual (타이밍 누출 방지). oauth.ts 도 사용. */
8
+ export declare function timingSafeEqualStr(a: string, b: string): boolean;
9
+ /**
10
+ * 직원별 리소스 스코프 (app_users 의 allowed_* 컬럼에서 옴).
11
+ * 각 필드 null/undefined/빈배열 = 그 차원은 전체 허용(미설정). admin role 은 scope 무시 전체.
12
+ */
13
+ export type ResourceScopes = {
14
+ slackChannels?: string[] | null;
15
+ driveFolders?: string[] | null;
16
+ tools?: string[] | null;
17
+ };
18
+ export type AuthContext = {
19
+ ok: boolean;
20
+ reason?: string;
21
+ org_id?: string;
22
+ role?: Role;
23
+ email?: string;
24
+ key_id?: string;
25
+ scopes?: ResourceScopes;
26
+ /** act-as(내부 프록시 위임) 경로로 들어온 요청인가. 프록시 키 유출에 대비해 고권한 도구를 막을 때 쓴다. */
27
+ actAs?: boolean;
28
+ };
29
+ export declare function authenticateApiKey(key: string | undefined): Promise<AuthContext>;
30
+ /**
31
+ * 도구별 role 가드. 예: 쓰기 도구는 admin 만.
32
+ *
33
+ * 사용 예:
34
+ * if (!requireRole(auth, 'admin', 'member')) {
35
+ * return errorResponse('forbidden — role insufficient');
36
+ * }
37
+ */
38
+ export declare function requireRole(auth: AuthContext, ...allowed: Role[]): boolean;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Knowledge MCP — API key auth.
3
+ *
4
+ * Keys are checked against the store (SQLite file or Supabase `validate_mcp_key`). Only a SHA-256 hash
5
+ * of each key is stored. Create keys with `knowledge-mcp keys create`.
6
+ */
7
+ import { createHash, timingSafeEqual } from 'crypto';
8
+ import { getStore } from '../store/index.js';
9
+ import { getConfig } from './config.js';
10
+ /** 상수시간 문자열 비교 — sha256 으로 길이 정규화 후 timingSafeEqual (타이밍 누출 방지). oauth.ts 도 사용. */
11
+ export function timingSafeEqualStr(a, b) {
12
+ const ha = createHash('sha256').update(a).digest();
13
+ const hb = createHash('sha256').update(b).digest();
14
+ return timingSafeEqual(ha, hb);
15
+ }
16
+ // ── 인증 결과 TTL 캐시 ──
17
+ // HTTP transport 는 도구 호출 1건 = 요청 1건 = validate_mcp_key RPC 1회.
18
+ // 챗봇 ReAct 루프(여러 도구)·여러 사용자가 몰리면 같은 key 로 RPC 가 다발 →
19
+ // 짧은 TTL 캐시로 DB 왕복을 줄인다. (key revoke 반영 지연은 최대 TTL 초)
20
+ const AUTH_CACHE_TTL_MS = Number(process.env.AUTH_CACHE_TTL_MS ?? 60_000);
21
+ const AUTH_CACHE_MAX = 500; // hard cap 백스톱 — key 회전이 많아도 무한 증가 방지(B-15)
22
+ const _authCache = new Map();
23
+ /** 만료 항목 제거 + cap 초과 시 가장 오래된 것부터 정리 (단일 인스턴스 in-memory 캐시 누수 방지). */
24
+ function setAuthCache(key, auth) {
25
+ const now = Date.now();
26
+ for (const [k, v] of _authCache)
27
+ if (v.expiresAt <= now)
28
+ _authCache.delete(k);
29
+ while (_authCache.size >= AUTH_CACHE_MAX) {
30
+ const oldest = _authCache.keys().next().value; // Map 삽입 순서 = 오래된 것 먼저
31
+ if (oldest === undefined)
32
+ break;
33
+ _authCache.delete(oldest);
34
+ }
35
+ _authCache.set(key, { auth, expiresAt: now + AUTH_CACHE_TTL_MS });
36
+ }
37
+ export async function authenticateApiKey(key) {
38
+ if (!key) {
39
+ return { ok: false, reason: 'MCP_API_KEY not provided' };
40
+ }
41
+ // Day 1 호환: 단일 dev key — **stdio(로컬 dev) 전용**.
42
+ // HTTP(production)에선 비활성(P2): 정적 문자열 1개가 감사 식별자 없는 admin 백도어가 되므로,
43
+ // 프로덕션 인증은 mcp_keys(해시 저장·revoke·expiry) 경로만 허용한다.
44
+ const devKey = process.env.MCP_DEV_KEY;
45
+ const transport = (process.env.MCP_TRANSPORT || 'http').toLowerCase();
46
+ if (devKey && transport === 'stdio' && timingSafeEqualStr(key, devKey)) {
47
+ return {
48
+ ok: true,
49
+ org_id: getConfig().orgId,
50
+ role: 'admin',
51
+ email: 'dev@local',
52
+ };
53
+ }
54
+ // 캐시 hit (성공 결과만 캐시 — 실패는 매번 재검증해 빠른 키 활성화 반영)
55
+ const cached = _authCache.get(key);
56
+ if (cached && cached.expiresAt > Date.now()) {
57
+ return cached.auth;
58
+ }
59
+ // 저장소(SQLite 파일 또는 Supabase validate_mcp_key RPC)에서 해시로 조회
60
+ try {
61
+ const store = await getStore();
62
+ const r = await store.validateKey(key);
63
+ if (!r.ok)
64
+ return { ok: false, reason: r.reason };
65
+ const auth = {
66
+ ok: true,
67
+ org_id: r.orgId ?? getConfig().orgId,
68
+ role: r.role,
69
+ email: r.email ?? undefined,
70
+ key_id: r.keyId,
71
+ };
72
+ setAuthCache(key, auth);
73
+ return auth;
74
+ }
75
+ catch (e) {
76
+ const msg = e instanceof Error ? e.message : String(e);
77
+ return { ok: false, reason: `auth error: ${msg}` };
78
+ }
79
+ }
80
+ /**
81
+ * 도구별 role 가드. 예: 쓰기 도구는 admin 만.
82
+ *
83
+ * 사용 예:
84
+ * if (!requireRole(auth, 'admin', 'member')) {
85
+ * return errorResponse('forbidden — role insufficient');
86
+ * }
87
+ */
88
+ export function requireRole(auth, ...allowed) {
89
+ if (!auth.ok || !auth.role)
90
+ return false;
91
+ return allowed.includes(auth.role);
92
+ }
93
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,259 @@
1
+ import { z } from 'zod';
2
+ /** First line of the server instructions. Each enabled plugin appends its own notes (plugin `instructions`). */
3
+ export declare const BASE_INSTRUCTIONS = "Knowledge MCP \u2014 tools over your team's data. Answer only from tool results; do not speculate.";
4
+ /**
5
+ * A Drive folder to index. `source` is an indexing hint:
6
+ * - "dev": structured knowledge docs (Markdown with frontmatter)
7
+ * - "biz": general documents (PDF/DOCX/etc.)
8
+ * If you have one kind of content, just use "dev" for everything.
9
+ */
10
+ declare const DriveRootSchema: z.ZodObject<{
11
+ id: z.ZodString;
12
+ name: z.ZodString;
13
+ source: z.ZodEnum<["dev", "biz"]>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ id: string;
16
+ name: string;
17
+ source: "dev" | "biz";
18
+ }, {
19
+ id: string;
20
+ name: string;
21
+ source: "dev" | "biz";
22
+ }>;
23
+ declare const UpstreamConfigSchema: z.ZodObject<{
24
+ /** Permission group and log name. Lowercase letters, digits and dashes. */
25
+ name: z.ZodString;
26
+ /** Streamable HTTP endpoint, e.g. https://mcp.example.com/mcp */
27
+ url: z.ZodString;
28
+ /** Tool name prefix. Default "<name>_" with dashes turned into underscores. */
29
+ prefix: z.ZodOptional<z.ZodString>;
30
+ /** Name of the env var holding the bearer token (the token itself never goes in this file). */
31
+ tokenEnv: z.ZodOptional<z.ZodString>;
32
+ /** Extra request headers (no secrets — use tokenEnv). */
33
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
34
+ /** "read" (default): only read-verb tools. "all": everything. Or an explicit list of remote tool names. */
35
+ expose: z.ZodDefault<z.ZodUnion<[z.ZodEnum<["read", "all"]>, z.ZodArray<z.ZodString, "many">]>>;
36
+ }, "strict", z.ZodTypeAny, {
37
+ url: string;
38
+ name: string;
39
+ expose: string[] | "read" | "all";
40
+ prefix?: string | undefined;
41
+ tokenEnv?: string | undefined;
42
+ headers?: Record<string, string> | undefined;
43
+ }, {
44
+ url: string;
45
+ name: string;
46
+ prefix?: string | undefined;
47
+ tokenEnv?: string | undefined;
48
+ headers?: Record<string, string> | undefined;
49
+ expose?: string[] | "read" | "all" | undefined;
50
+ }>;
51
+ declare const ConfigSchema: z.ZodObject<{
52
+ /** Organization identifier — flows into the auth context as org_id. */
53
+ orgId: z.ZodDefault<z.ZodString>;
54
+ branding: z.ZodDefault<z.ZodObject<{
55
+ name: z.ZodDefault<z.ZodString>;
56
+ /** Replaces the generated instructions (base line + each enabled plugin's notes) when set. */
57
+ instructions: z.ZodOptional<z.ZodString>;
58
+ }, "strip", z.ZodTypeAny, {
59
+ name: string;
60
+ instructions?: string | undefined;
61
+ }, {
62
+ name?: string | undefined;
63
+ instructions?: string | undefined;
64
+ }>>;
65
+ drive: z.ZodDefault<z.ZodObject<{
66
+ /** SSOT for index/search scope. Empty = Drive indexing disabled (tools still gated by credentials). */
67
+ roots: z.ZodDefault<z.ZodArray<z.ZodObject<{
68
+ id: z.ZodString;
69
+ name: z.ZodString;
70
+ source: z.ZodEnum<["dev", "biz"]>;
71
+ }, "strip", z.ZodTypeAny, {
72
+ id: string;
73
+ name: string;
74
+ source: "dev" | "biz";
75
+ }, {
76
+ id: string;
77
+ name: string;
78
+ source: "dev" | "biz";
79
+ }>, "many">>;
80
+ /** Folder names to exclude from the search index (sensitive/backup). Merged with built-in dev-artifact names (node_modules etc.). */
81
+ excludeFolders: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
82
+ }, "strip", z.ZodTypeAny, {
83
+ roots: {
84
+ id: string;
85
+ name: string;
86
+ source: "dev" | "biz";
87
+ }[];
88
+ excludeFolders: string[];
89
+ }, {
90
+ roots?: {
91
+ id: string;
92
+ name: string;
93
+ source: "dev" | "biz";
94
+ }[] | undefined;
95
+ excludeFolders?: string[] | undefined;
96
+ }>>;
97
+ qdrant: z.ZodDefault<z.ZodObject<{
98
+ /** Vector collection name. If env QDRANT_COLLECTION is set, it takes precedence (zero-downtime swap). */
99
+ collection: z.ZodDefault<z.ZodString>;
100
+ }, "strip", z.ZodTypeAny, {
101
+ collection: string;
102
+ }, {
103
+ collection?: string | undefined;
104
+ }>>;
105
+ data: z.ZodDefault<z.ZodObject<{
106
+ /** Supabase schema holding the server's own tables (oauth clients, refresh tokens, sync_state, tool_calls, app_users). */
107
+ schema: z.ZodDefault<z.ZodString>;
108
+ }, "strip", z.ZodTypeAny, {
109
+ schema: string;
110
+ }, {
111
+ schema?: string | undefined;
112
+ }>>;
113
+ /** Your own domains (used e.g. to distinguish first-party from external sources). */
114
+ selfDomains: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
115
+ oauth: z.ZodDefault<z.ZodObject<{
116
+ /**
117
+ * Google Workspace domains allowed to sign in (the account's `hd` claim must match).
118
+ * env OAUTH_ALLOWED_HD (comma-separated) takes precedence.
119
+ */
120
+ allowedDomains: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
121
+ /**
122
+ * Individual emails allowed to sign in, in addition to the domains (any Google account type).
123
+ * env OAUTH_ALLOWED_EMAILS (comma-separated) takes precedence.
124
+ * If both lists are empty, OAuth stays off — otherwise any Google account could sign in.
125
+ */
126
+ allowedEmails: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
127
+ }, "strip", z.ZodTypeAny, {
128
+ allowedDomains: string[];
129
+ allowedEmails: string[];
130
+ }, {
131
+ allowedDomains?: string[] | undefined;
132
+ allowedEmails?: string[] | undefined;
133
+ }>>;
134
+ access: z.ZodDefault<z.ZodObject<{
135
+ /**
136
+ * Tool groups a signed-in user WITHOUT an app_users row may use (role "viewer").
137
+ * A group is a plugin id (drive, slack, github, railway, or your own) or a config upstream name.
138
+ * Default: none, so unregistered users only get `ping`.
139
+ */
140
+ viewerTools: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
141
+ }, "strip", z.ZodTypeAny, {
142
+ viewerTools: string[];
143
+ }, {
144
+ viewerTools?: string[] | undefined;
145
+ }>>;
146
+ /** Per-plugin settings: `plugins.<id>` is passed to that plugin as ctx.config. */
147
+ plugins: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
148
+ /** Remote MCP servers to proxy. Their tools are listed as `<prefix><name>` in the group `name`. */
149
+ upstreams: z.ZodDefault<z.ZodArray<z.ZodObject<{
150
+ /** Permission group and log name. Lowercase letters, digits and dashes. */
151
+ name: z.ZodString;
152
+ /** Streamable HTTP endpoint, e.g. https://mcp.example.com/mcp */
153
+ url: z.ZodString;
154
+ /** Tool name prefix. Default "<name>_" with dashes turned into underscores. */
155
+ prefix: z.ZodOptional<z.ZodString>;
156
+ /** Name of the env var holding the bearer token (the token itself never goes in this file). */
157
+ tokenEnv: z.ZodOptional<z.ZodString>;
158
+ /** Extra request headers (no secrets — use tokenEnv). */
159
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
160
+ /** "read" (default): only read-verb tools. "all": everything. Or an explicit list of remote tool names. */
161
+ expose: z.ZodDefault<z.ZodUnion<[z.ZodEnum<["read", "all"]>, z.ZodArray<z.ZodString, "many">]>>;
162
+ }, "strict", z.ZodTypeAny, {
163
+ url: string;
164
+ name: string;
165
+ expose: string[] | "read" | "all";
166
+ prefix?: string | undefined;
167
+ tokenEnv?: string | undefined;
168
+ headers?: Record<string, string> | undefined;
169
+ }, {
170
+ url: string;
171
+ name: string;
172
+ prefix?: string | undefined;
173
+ tokenEnv?: string | undefined;
174
+ headers?: Record<string, string> | undefined;
175
+ expose?: string[] | "read" | "all" | undefined;
176
+ }>, "many">>;
177
+ }, "strict", z.ZodTypeAny, {
178
+ data: {
179
+ schema: string;
180
+ };
181
+ orgId: string;
182
+ branding: {
183
+ name: string;
184
+ instructions?: string | undefined;
185
+ };
186
+ drive: {
187
+ roots: {
188
+ id: string;
189
+ name: string;
190
+ source: "dev" | "biz";
191
+ }[];
192
+ excludeFolders: string[];
193
+ };
194
+ qdrant: {
195
+ collection: string;
196
+ };
197
+ selfDomains: string[];
198
+ oauth: {
199
+ allowedDomains: string[];
200
+ allowedEmails: string[];
201
+ };
202
+ access: {
203
+ viewerTools: string[];
204
+ };
205
+ plugins: Record<string, unknown>;
206
+ upstreams: {
207
+ url: string;
208
+ name: string;
209
+ expose: string[] | "read" | "all";
210
+ prefix?: string | undefined;
211
+ tokenEnv?: string | undefined;
212
+ headers?: Record<string, string> | undefined;
213
+ }[];
214
+ }, {
215
+ data?: {
216
+ schema?: string | undefined;
217
+ } | undefined;
218
+ orgId?: string | undefined;
219
+ branding?: {
220
+ name?: string | undefined;
221
+ instructions?: string | undefined;
222
+ } | undefined;
223
+ drive?: {
224
+ roots?: {
225
+ id: string;
226
+ name: string;
227
+ source: "dev" | "biz";
228
+ }[] | undefined;
229
+ excludeFolders?: string[] | undefined;
230
+ } | undefined;
231
+ qdrant?: {
232
+ collection?: string | undefined;
233
+ } | undefined;
234
+ selfDomains?: string[] | undefined;
235
+ oauth?: {
236
+ allowedDomains?: string[] | undefined;
237
+ allowedEmails?: string[] | undefined;
238
+ } | undefined;
239
+ access?: {
240
+ viewerTools?: string[] | undefined;
241
+ } | undefined;
242
+ plugins?: Record<string, unknown> | undefined;
243
+ upstreams?: {
244
+ url: string;
245
+ name: string;
246
+ prefix?: string | undefined;
247
+ tokenEnv?: string | undefined;
248
+ headers?: Record<string, string> | undefined;
249
+ expose?: string[] | "read" | "all" | undefined;
250
+ }[] | undefined;
251
+ }>;
252
+ export type AppConfig = z.infer<typeof ConfigSchema>;
253
+ export type DriveRoot = z.infer<typeof DriveRootSchema>;
254
+ export type UpstreamConfig = z.infer<typeof UpstreamConfigSchema>;
255
+ /** Validated config (singleton). Loads and validates on first call; invalid config fails fast with a clear error. */
256
+ export declare function getConfig(): AppConfig;
257
+ /** Which config source was chosen (for boot logs / diagnostics). Valid after getConfig(). */
258
+ export declare function configSource(): string;
259
+ export {};
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Deployment configuration — "what to look at" is kept out of the code.
3
+ *
4
+ * Credentials live in env (Supabase / Qdrant / Drive / Slack / GitHub keys). This module holds the
5
+ * rest: which Drive folders, which Qdrant collection, which Supabase schema, which domains. That
6
+ * lets anyone self-host by supplying their own config file — no code changes.
7
+ *
8
+ * Resolution order (first match wins):
9
+ * 1. MCP_CONFIG_JSON — inline JSON (deploy-friendly: one env var, no file)
10
+ * 2. MCP_CONFIG_PATH — explicit path to a JSON file
11
+ * 3. mcp.config.json — in the working directory, then in the app root (see paths.ts)
12
+ * 4. (none) — neutral built-in defaults (server boots; Drive indexing stays off until
13
+ * `drive.roots` is set)
14
+ *
15
+ * ⚠️ No secrets here. Never put keys/tokens in this file or the config JSON — those belong in env.
16
+ * This is only "what to look at".
17
+ */
18
+ import { existsSync, readFileSync } from 'node:fs';
19
+ import { z } from 'zod';
20
+ import { candidates } from './paths.js';
21
+ /** First line of the server instructions. Each enabled plugin appends its own notes (plugin `instructions`). */
22
+ export const BASE_INSTRUCTIONS = 'Knowledge MCP — tools over your team\'s data. Answer only from tool results; do not speculate.';
23
+ /**
24
+ * A Drive folder to index. `source` is an indexing hint:
25
+ * - "dev": structured knowledge docs (Markdown with frontmatter)
26
+ * - "biz": general documents (PDF/DOCX/etc.)
27
+ * If you have one kind of content, just use "dev" for everything.
28
+ */
29
+ const DriveRootSchema = z.object({
30
+ id: z.string().min(1),
31
+ name: z.string().min(1),
32
+ source: z.enum(['dev', 'biz']),
33
+ });
34
+ const UpstreamConfigSchema = z
35
+ .object({
36
+ /** Permission group and log name. Lowercase letters, digits and dashes. */
37
+ name: z.string().regex(/^[a-z][a-z0-9-]{0,31}$/),
38
+ /** Streamable HTTP endpoint, e.g. https://mcp.example.com/mcp */
39
+ url: z.string().url(),
40
+ /** Tool name prefix. Default "<name>_" with dashes turned into underscores. */
41
+ prefix: z.string().regex(/^[a-z][a-z0-9_]*_$/).optional(),
42
+ /** Name of the env var holding the bearer token (the token itself never goes in this file). */
43
+ tokenEnv: z.string().optional(),
44
+ /** Extra request headers (no secrets — use tokenEnv). */
45
+ headers: z.record(z.string()).optional(),
46
+ /** "read" (default): only read-verb tools. "all": everything. Or an explicit list of remote tool names. */
47
+ expose: z.union([z.enum(['read', 'all']), z.array(z.string())]).default('read'),
48
+ })
49
+ .strict();
50
+ const ConfigSchema = z
51
+ .object({
52
+ /** Organization identifier — flows into the auth context as org_id. */
53
+ orgId: z.string().default('org'),
54
+ branding: z
55
+ .object({
56
+ name: z.string().default('Knowledge MCP'),
57
+ /** Replaces the generated instructions (base line + each enabled plugin's notes) when set. */
58
+ instructions: z.string().optional(),
59
+ })
60
+ .default({}),
61
+ drive: z
62
+ .object({
63
+ /** SSOT for index/search scope. Empty = Drive indexing disabled (tools still gated by credentials). */
64
+ roots: z.array(DriveRootSchema).default([]),
65
+ /** Folder names to exclude from the search index (sensitive/backup). Merged with built-in dev-artifact names (node_modules etc.). */
66
+ excludeFolders: z.array(z.string()).default([]),
67
+ })
68
+ .default({}),
69
+ qdrant: z
70
+ .object({
71
+ /** Vector collection name. If env QDRANT_COLLECTION is set, it takes precedence (zero-downtime swap). */
72
+ collection: z.string().default('knowledge_v1'),
73
+ })
74
+ .default({}),
75
+ data: z
76
+ .object({
77
+ /** Supabase schema holding the server's own tables (oauth clients, refresh tokens, sync_state, tool_calls, app_users). */
78
+ schema: z.string().default('public'),
79
+ })
80
+ .default({}),
81
+ /** Your own domains (used e.g. to distinguish first-party from external sources). */
82
+ selfDomains: z.array(z.string()).default([]),
83
+ oauth: z
84
+ .object({
85
+ /**
86
+ * Google Workspace domains allowed to sign in (the account's `hd` claim must match).
87
+ * env OAUTH_ALLOWED_HD (comma-separated) takes precedence.
88
+ */
89
+ allowedDomains: z.array(z.string()).default([]),
90
+ /**
91
+ * Individual emails allowed to sign in, in addition to the domains (any Google account type).
92
+ * env OAUTH_ALLOWED_EMAILS (comma-separated) takes precedence.
93
+ * If both lists are empty, OAuth stays off — otherwise any Google account could sign in.
94
+ */
95
+ allowedEmails: z.array(z.string()).default([]),
96
+ })
97
+ .default({}),
98
+ access: z
99
+ .object({
100
+ /**
101
+ * Tool groups a signed-in user WITHOUT an app_users row may use (role "viewer").
102
+ * A group is a plugin id (drive, slack, github, railway, or your own) or a config upstream name.
103
+ * Default: none, so unregistered users only get `ping`.
104
+ */
105
+ viewerTools: z.array(z.string()).default([]),
106
+ })
107
+ .default({}),
108
+ /** Per-plugin settings: `plugins.<id>` is passed to that plugin as ctx.config. */
109
+ plugins: z.record(z.unknown()).default({}),
110
+ /** Remote MCP servers to proxy. Their tools are listed as `<prefix><name>` in the group `name`. */
111
+ upstreams: z.array(UpstreamConfigSchema).default([]),
112
+ })
113
+ .strict();
114
+ let _source = 'defaults';
115
+ /** Load the raw config object, or null if none is provided. Explicit/inline parse failures throw (fail fast). */
116
+ function loadRawConfig() {
117
+ const inline = process.env.MCP_CONFIG_JSON;
118
+ if (inline && inline.trim()) {
119
+ try {
120
+ _source = 'env:MCP_CONFIG_JSON';
121
+ return JSON.parse(inline);
122
+ }
123
+ catch (e) {
124
+ throw new Error(`[config] failed to parse MCP_CONFIG_JSON: ${e instanceof Error ? e.message : String(e)}`);
125
+ }
126
+ }
127
+ const explicitPath = process.env.MCP_CONFIG_PATH;
128
+ if (explicitPath) {
129
+ try {
130
+ const raw = readFileSync(explicitPath, 'utf8');
131
+ _source = `file:${explicitPath}`;
132
+ return JSON.parse(raw);
133
+ }
134
+ catch (e) {
135
+ throw new Error(`[config] failed to read MCP_CONFIG_PATH (${explicitPath}): ${e instanceof Error ? e.message : String(e)}`);
136
+ }
137
+ }
138
+ // Default locations may simply not exist → built-in defaults. A file that exists but is broken fails fast.
139
+ const path = candidates('mcp.config.json').find((p) => existsSync(p));
140
+ if (!path)
141
+ return null;
142
+ try {
143
+ _source = `file:${path}`;
144
+ return JSON.parse(readFileSync(path, 'utf8'));
145
+ }
146
+ catch (e) {
147
+ throw new Error(`[config] failed to parse ${path}: ${e instanceof Error ? e.message : String(e)}`);
148
+ }
149
+ }
150
+ let _config = null;
151
+ /** Validated config (singleton). Loads and validates on first call; invalid config fails fast with a clear error. */
152
+ export function getConfig() {
153
+ if (_config)
154
+ return _config;
155
+ const raw = loadRawConfig();
156
+ try {
157
+ _config = ConfigSchema.parse(raw === null ? {} : raw);
158
+ }
159
+ catch (e) {
160
+ if (e instanceof z.ZodError) {
161
+ const detail = e.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`).join('; ');
162
+ throw new Error(`[config] validation failed — ${detail}`);
163
+ }
164
+ throw e;
165
+ }
166
+ return _config;
167
+ }
168
+ /** Which config source was chosen (for boot logs / diagnostics). Valid after getConfig(). */
169
+ export function configSource() {
170
+ return _source;
171
+ }
172
+ //# sourceMappingURL=config.js.map