@nimbus-dev/connectors 0.1.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 (301) hide show
  1. package/LICENSE +662 -0
  2. package/NOTICE +40 -0
  3. package/README.md +111 -0
  4. package/connectors/airflow/nimbus.extension.json +16 -0
  5. package/connectors/airflow/src/search-filter.ts +38 -0
  6. package/connectors/airflow/src/server.ts +91 -0
  7. package/connectors/apple/nimbus.extension.json +40 -0
  8. package/connectors/apple/src/apple-mail-core.ts +43 -0
  9. package/connectors/apple/src/caldav-core.ts +116 -0
  10. package/connectors/apple/src/calendar-tools.ts +278 -0
  11. package/connectors/apple/src/server.ts +541 -0
  12. package/connectors/apple/src/tools.ts +138 -0
  13. package/connectors/argocd/nimbus.extension.json +16 -0
  14. package/connectors/argocd/src/search-filter.ts +24 -0
  15. package/connectors/argocd/src/server.ts +158 -0
  16. package/connectors/athena/nimbus.extension.json +15 -0
  17. package/connectors/athena/src/server.ts +6 -0
  18. package/connectors/athena/src/tools.ts +181 -0
  19. package/connectors/aws/nimbus.extension.json +21 -0
  20. package/connectors/aws/src/server.ts +191 -0
  21. package/connectors/azure/nimbus.extension.json +15 -0
  22. package/connectors/azure/src/server.ts +139 -0
  23. package/connectors/bigeye/nimbus.extension.json +16 -0
  24. package/connectors/bigeye/src/search-filter.ts +22 -0
  25. package/connectors/bigeye/src/server.ts +161 -0
  26. package/connectors/bigquery/nimbus.extension.json +15 -0
  27. package/connectors/bigquery/src/server.ts +6 -0
  28. package/connectors/bigquery/src/tools.ts +175 -0
  29. package/connectors/bitbucket/nimbus.extension.json +15 -0
  30. package/connectors/bitbucket/src/server.ts +252 -0
  31. package/connectors/bitrise/nimbus.extension.json +15 -0
  32. package/connectors/bitrise/src/search-filter.ts +11 -0
  33. package/connectors/bitrise/src/server.ts +83 -0
  34. package/connectors/canva/nimbus.extension.json +15 -0
  35. package/connectors/canva/src/search-filter.ts +23 -0
  36. package/connectors/canva/src/server.ts +77 -0
  37. package/connectors/circleci/nimbus.extension.json +15 -0
  38. package/connectors/circleci/src/server.ts +184 -0
  39. package/connectors/cloud-logging/nimbus.extension.json +15 -0
  40. package/connectors/cloud-logging/src/server.ts +6 -0
  41. package/connectors/cloud-logging/src/tools.ts +115 -0
  42. package/connectors/cloudwatch/nimbus.extension.json +15 -0
  43. package/connectors/cloudwatch/src/server.ts +6 -0
  44. package/connectors/cloudwatch/src/tools.ts +144 -0
  45. package/connectors/codemagic/nimbus.extension.json +15 -0
  46. package/connectors/codemagic/src/search-filter.ts +11 -0
  47. package/connectors/codemagic/src/server.ts +74 -0
  48. package/connectors/confluence/nimbus.extension.json +15 -0
  49. package/connectors/confluence/src/kb-append.ts +25 -0
  50. package/connectors/confluence/src/server.ts +309 -0
  51. package/connectors/dagster/nimbus.extension.json +16 -0
  52. package/connectors/dagster/src/search-filter.ts +53 -0
  53. package/connectors/dagster/src/server.ts +218 -0
  54. package/connectors/databricks/nimbus.extension.json +16 -0
  55. package/connectors/databricks/src/search-filter.ts +28 -0
  56. package/connectors/databricks/src/server.ts +81 -0
  57. package/connectors/datadog/nimbus.extension.json +15 -0
  58. package/connectors/datadog/src/server.ts +56 -0
  59. package/connectors/dataprofile/nimbus.extension.json +16 -0
  60. package/connectors/dataprofile/src/profile.ts +298 -0
  61. package/connectors/dataprofile/src/server.ts +6 -0
  62. package/connectors/dataprofile/src/tools.ts +69 -0
  63. package/connectors/dbt/nimbus.extension.json +16 -0
  64. package/connectors/dbt/src/search-filter.ts +28 -0
  65. package/connectors/dbt/src/server.ts +96 -0
  66. package/connectors/dependencytrack/nimbus.extension.json +16 -0
  67. package/connectors/dependencytrack/src/search-filter.ts +24 -0
  68. package/connectors/dependencytrack/src/server.ts +80 -0
  69. package/connectors/discord/nimbus.extension.json +15 -0
  70. package/connectors/discord/src/server.ts +93 -0
  71. package/connectors/elasticsearch/nimbus.extension.json +16 -0
  72. package/connectors/elasticsearch/src/server.ts +6 -0
  73. package/connectors/elasticsearch/src/tools.ts +110 -0
  74. package/connectors/fastmail/nimbus.extension.json +16 -0
  75. package/connectors/fastmail/src/jmap-core.ts +86 -0
  76. package/connectors/fastmail/src/server.ts +194 -0
  77. package/connectors/fastmail/src/tools.ts +143 -0
  78. package/connectors/figma/nimbus.extension.json +15 -0
  79. package/connectors/figma/src/search-filter.ts +24 -0
  80. package/connectors/figma/src/server.ts +117 -0
  81. package/connectors/firebase/nimbus.extension.json +15 -0
  82. package/connectors/firebase/src/search-filter.ts +30 -0
  83. package/connectors/firebase/src/server.ts +94 -0
  84. package/connectors/firebase/src/token.ts +41 -0
  85. package/connectors/flagsmith/nimbus.extension.json +15 -0
  86. package/connectors/flagsmith/src/search-filter.ts +29 -0
  87. package/connectors/flagsmith/src/server.ts +102 -0
  88. package/connectors/flux/nimbus.extension.json +16 -0
  89. package/connectors/flux/src/search-filter.ts +42 -0
  90. package/connectors/flux/src/server.ts +182 -0
  91. package/connectors/gcp/nimbus.extension.json +15 -0
  92. package/connectors/gcp/src/server.ts +140 -0
  93. package/connectors/github/nimbus.extension.json +15 -0
  94. package/connectors/github/src/server.ts +347 -0
  95. package/connectors/github-actions/nimbus.extension.json +15 -0
  96. package/connectors/github-actions/src/server.ts +213 -0
  97. package/connectors/gitlab/nimbus.extension.json +15 -0
  98. package/connectors/gitlab/src/server.ts +392 -0
  99. package/connectors/gmail/nimbus.extension.json +15 -0
  100. package/connectors/gmail/src/server.ts +277 -0
  101. package/connectors/google-drive/nimbus.extension.json +15 -0
  102. package/connectors/google-drive/src/drive-query.ts +3 -0
  103. package/connectors/google-drive/src/server.ts +531 -0
  104. package/connectors/google-meet/nimbus.extension.json +15 -0
  105. package/connectors/google-meet/src/server.ts +89 -0
  106. package/connectors/google-photos/nimbus.extension.json +15 -0
  107. package/connectors/google-photos/src/server.ts +139 -0
  108. package/connectors/grafana/nimbus.extension.json +15 -0
  109. package/connectors/grafana/src/server.ts +61 -0
  110. package/connectors/great-expectations/nimbus.extension.json +16 -0
  111. package/connectors/great-expectations/src/gx-parse.ts +292 -0
  112. package/connectors/great-expectations/src/server.ts +6 -0
  113. package/connectors/great-expectations/src/tools.ts +91 -0
  114. package/connectors/greenhouse/nimbus.extension.json +15 -0
  115. package/connectors/greenhouse/src/search-filter.ts +65 -0
  116. package/connectors/greenhouse/src/server.ts +67 -0
  117. package/connectors/hubspot/nimbus.extension.json +15 -0
  118. package/connectors/hubspot/src/search-filter.ts +28 -0
  119. package/connectors/hubspot/src/server.ts +81 -0
  120. package/connectors/iac/nimbus.extension.json +14 -0
  121. package/connectors/iac/src/server.ts +145 -0
  122. package/connectors/imap/nimbus.extension.json +16 -0
  123. package/connectors/imap/src/imap-core.ts +110 -0
  124. package/connectors/imap/src/server.ts +232 -0
  125. package/connectors/imap/src/tools.ts +40 -0
  126. package/connectors/intercom/nimbus.extension.json +15 -0
  127. package/connectors/intercom/src/search-filter.ts +46 -0
  128. package/connectors/intercom/src/server.ts +65 -0
  129. package/connectors/jenkins/nimbus.extension.json +15 -0
  130. package/connectors/jenkins/src/jenkins-api.ts +156 -0
  131. package/connectors/jenkins/src/server.ts +223 -0
  132. package/connectors/jira/nimbus.extension.json +15 -0
  133. package/connectors/jira/src/server.ts +309 -0
  134. package/connectors/kubernetes/nimbus.extension.json +15 -0
  135. package/connectors/kubernetes/src/server.ts +163 -0
  136. package/connectors/launchdarkly/nimbus.extension.json +15 -0
  137. package/connectors/launchdarkly/src/search-filter.ts +31 -0
  138. package/connectors/launchdarkly/src/server.ts +79 -0
  139. package/connectors/lever/nimbus.extension.json +15 -0
  140. package/connectors/lever/src/search-filter.ts +35 -0
  141. package/connectors/lever/src/server.ts +64 -0
  142. package/connectors/linear/nimbus.extension.json +15 -0
  143. package/connectors/linear/src/server.ts +467 -0
  144. package/connectors/localdb/nimbus.extension.json +16 -0
  145. package/connectors/localdb/src/server.ts +6 -0
  146. package/connectors/localdb/src/sql-scan.ts +158 -0
  147. package/connectors/localdb/src/tools.ts +67 -0
  148. package/connectors/looker/nimbus.extension.json +16 -0
  149. package/connectors/looker/src/search-filter.ts +22 -0
  150. package/connectors/looker/src/server.ts +254 -0
  151. package/connectors/mendeley/nimbus.extension.json +15 -0
  152. package/connectors/mendeley/src/search-filter.ts +68 -0
  153. package/connectors/mendeley/src/server.ts +59 -0
  154. package/connectors/mercury/nimbus.extension.json +15 -0
  155. package/connectors/mercury/src/search-filter.ts +11 -0
  156. package/connectors/mercury/src/server.ts +61 -0
  157. package/connectors/metabase/nimbus.extension.json +16 -0
  158. package/connectors/metabase/src/search-filter.ts +22 -0
  159. package/connectors/metabase/src/server.ts +83 -0
  160. package/connectors/miro/nimbus.extension.json +15 -0
  161. package/connectors/miro/src/search-filter.ts +24 -0
  162. package/connectors/miro/src/server.ts +76 -0
  163. package/connectors/mlflow/nimbus.extension.json +16 -0
  164. package/connectors/mlflow/src/search-filter.ts +38 -0
  165. package/connectors/mlflow/src/server.ts +167 -0
  166. package/connectors/monte-carlo/nimbus.extension.json +16 -0
  167. package/connectors/monte-carlo/src/search-filter.ts +11 -0
  168. package/connectors/monte-carlo/src/server.ts +241 -0
  169. package/connectors/netlify/nimbus.extension.json +15 -0
  170. package/connectors/netlify/src/search-filter.ts +37 -0
  171. package/connectors/netlify/src/server.ts +64 -0
  172. package/connectors/newrelic/nimbus.extension.json +15 -0
  173. package/connectors/newrelic/src/server.ts +48 -0
  174. package/connectors/notion/nimbus.extension.json +15 -0
  175. package/connectors/notion/src/kb-append.ts +23 -0
  176. package/connectors/notion/src/server.ts +343 -0
  177. package/connectors/obsidian/nimbus.extension.json +19 -0
  178. package/connectors/obsidian/src/server.ts +433 -0
  179. package/connectors/onedrive/nimbus.extension.json +15 -0
  180. package/connectors/onedrive/src/server.ts +256 -0
  181. package/connectors/outlook/nimbus.extension.json +15 -0
  182. package/connectors/outlook/src/server.ts +358 -0
  183. package/connectors/outlook/src/tool-scope-policy.ts +53 -0
  184. package/connectors/pagerduty/nimbus.extension.json +15 -0
  185. package/connectors/pagerduty/src/server.ts +140 -0
  186. package/connectors/pipedrive/nimbus.extension.json +15 -0
  187. package/connectors/pipedrive/src/search-filter.ts +11 -0
  188. package/connectors/pipedrive/src/server.ts +62 -0
  189. package/connectors/powerbi/nimbus.extension.json +16 -0
  190. package/connectors/powerbi/src/search-filter.ts +22 -0
  191. package/connectors/powerbi/src/server.ts +249 -0
  192. package/connectors/prefect/nimbus.extension.json +16 -0
  193. package/connectors/prefect/src/search-filter.ts +40 -0
  194. package/connectors/prefect/src/server.ts +94 -0
  195. package/connectors/protonmail/nimbus.extension.json +16 -0
  196. package/connectors/protonmail/src/mail-core.ts +96 -0
  197. package/connectors/protonmail/src/server.ts +231 -0
  198. package/connectors/protonmail/src/tools.ts +45 -0
  199. package/connectors/raindrop/nimbus.extension.json +15 -0
  200. package/connectors/raindrop/src/search-filter.ts +20 -0
  201. package/connectors/raindrop/src/server.ts +107 -0
  202. package/connectors/ramp/nimbus.extension.json +15 -0
  203. package/connectors/ramp/src/search-filter.ts +37 -0
  204. package/connectors/ramp/src/server.ts +109 -0
  205. package/connectors/readwise/nimbus.extension.json +15 -0
  206. package/connectors/readwise/src/search-filter.ts +64 -0
  207. package/connectors/readwise/src/server.ts +92 -0
  208. package/connectors/sagemaker/nimbus.extension.json +15 -0
  209. package/connectors/sagemaker/src/server.ts +6 -0
  210. package/connectors/sagemaker/src/tools.ts +121 -0
  211. package/connectors/salesforce/nimbus.extension.json +15 -0
  212. package/connectors/salesforce/src/search-filter.ts +23 -0
  213. package/connectors/salesforce/src/server.ts +94 -0
  214. package/connectors/semgrep/nimbus.extension.json +15 -0
  215. package/connectors/semgrep/src/search-filter.ts +31 -0
  216. package/connectors/semgrep/src/server.ts +108 -0
  217. package/connectors/sentry/nimbus.extension.json +15 -0
  218. package/connectors/sentry/src/server.ts +66 -0
  219. package/connectors/slack/nimbus.extension.json +15 -0
  220. package/connectors/slack/src/server.ts +360 -0
  221. package/connectors/snowflake/nimbus.extension.json +16 -0
  222. package/connectors/snowflake/src/search-filter.ts +26 -0
  223. package/connectors/snowflake/src/server.ts +236 -0
  224. package/connectors/snyk/nimbus.extension.json +15 -0
  225. package/connectors/snyk/src/search-filter.ts +28 -0
  226. package/connectors/snyk/src/server.ts +144 -0
  227. package/connectors/sonarqube/nimbus.extension.json +15 -0
  228. package/connectors/sonarqube/src/search-filter.ts +28 -0
  229. package/connectors/sonarqube/src/server.ts +115 -0
  230. package/connectors/stackoverflow/nimbus.extension.json +15 -0
  231. package/connectors/stackoverflow/src/search-filter.ts +52 -0
  232. package/connectors/stackoverflow/src/server.ts +81 -0
  233. package/connectors/storybook/nimbus.extension.json +16 -0
  234. package/connectors/storybook/src/server.ts +6 -0
  235. package/connectors/storybook/src/storybook-parse.ts +67 -0
  236. package/connectors/storybook/src/tools.ts +68 -0
  237. package/connectors/stripe/nimbus.extension.json +15 -0
  238. package/connectors/stripe/src/search-filter.ts +19 -0
  239. package/connectors/stripe/src/server.ts +65 -0
  240. package/connectors/superset/nimbus.extension.json +16 -0
  241. package/connectors/superset/src/search-filter.ts +22 -0
  242. package/connectors/superset/src/server.ts +119 -0
  243. package/connectors/tableau/nimbus.extension.json +16 -0
  244. package/connectors/tableau/src/search-filter.ts +22 -0
  245. package/connectors/tableau/src/server.ts +248 -0
  246. package/connectors/teams/nimbus.extension.json +20 -0
  247. package/connectors/teams/src/bot-send.ts +72 -0
  248. package/connectors/teams/src/server.ts +257 -0
  249. package/connectors/testflight/nimbus.extension.json +15 -0
  250. package/connectors/testflight/src/jwt.ts +27 -0
  251. package/connectors/testflight/src/search-filter.ts +30 -0
  252. package/connectors/testflight/src/server.ts +74 -0
  253. package/connectors/vercel/nimbus.extension.json +15 -0
  254. package/connectors/vercel/src/search-filter.ts +34 -0
  255. package/connectors/vercel/src/server.ts +79 -0
  256. package/connectors/vertex-ai/nimbus.extension.json +15 -0
  257. package/connectors/vertex-ai/src/server.ts +6 -0
  258. package/connectors/vertex-ai/src/tools.ts +145 -0
  259. package/connectors/wiz/nimbus.extension.json +15 -0
  260. package/connectors/wiz/src/search-filter.ts +50 -0
  261. package/connectors/wiz/src/server.ts +192 -0
  262. package/connectors/workday/nimbus.extension.json +19 -0
  263. package/connectors/workday/src/search-filter.ts +5 -0
  264. package/connectors/workday/src/server.ts +78 -0
  265. package/connectors/workday/src/tools.ts +1 -0
  266. package/connectors/zendesk/nimbus.extension.json +16 -0
  267. package/connectors/zendesk/src/search-filter.ts +11 -0
  268. package/connectors/zendesk/src/server.ts +77 -0
  269. package/connectors/zoom/nimbus.extension.json +15 -0
  270. package/connectors/zoom/src/encode-meeting-path.ts +5 -0
  271. package/connectors/zoom/src/recordings-window.ts +34 -0
  272. package/connectors/zoom/src/search-filter.ts +42 -0
  273. package/connectors/zoom/src/server.ts +98 -0
  274. package/connectors/zotero/nimbus.extension.json +15 -0
  275. package/connectors/zotero/src/search-filter.ts +81 -0
  276. package/connectors/zotero/src/server.ts +82 -0
  277. package/package.json +152 -0
  278. package/shared/atlassian-json-fetch.ts +42 -0
  279. package/shared/audit-chain.ts +133 -0
  280. package/shared/connector-mode.ts +60 -0
  281. package/shared/consent-kit.ts +323 -0
  282. package/shared/fetch-bearer-json.ts +18 -0
  283. package/shared/header-safe.ts +28 -0
  284. package/shared/imap-bodystructure.ts +113 -0
  285. package/shared/imap-mail-core.ts +57 -0
  286. package/shared/imap-tool-kit.ts +293 -0
  287. package/shared/join-api-path.ts +18 -0
  288. package/shared/kb-markdown.ts +142 -0
  289. package/shared/mcp-search-tool.ts +37 -0
  290. package/shared/mcp-tool-kit.ts +35 -0
  291. package/shared/nimbus-spawn.ts +128 -0
  292. package/shared/rest-tool-kit.ts +18 -0
  293. package/shared/run-cli-json.ts +55 -0
  294. package/shared/run-read-only-mcp-connector.ts +59 -0
  295. package/shared/safe-cli-arg.ts +55 -0
  296. package/shared/search-filter.ts +140 -0
  297. package/shared/strip-trailing-slashes.ts +7 -0
  298. package/shared/write-scope.ts +58 -0
  299. package/standalone/src/bin.ts +19 -0
  300. package/standalone/src/launcher.ts +186 -0
  301. package/standalone/tsconfig.json +8 -0
@@ -0,0 +1,433 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ fstatSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ readSync,
10
+ statSync,
11
+ writeSync,
12
+ } from "node:fs";
13
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
14
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { z } from "zod";
17
+
18
+ import {
19
+ createRegisterSimpleTool,
20
+ createZodToolRegistrar,
21
+ mcpJsonResult as jsonResult,
22
+ requireProcessEnv,
23
+ } from "../../../shared/mcp-tool-kit.ts";
24
+
25
+ const VAULT_MARKER = ".obsidian";
26
+ const DEFAULT_IGNORED_DIR_NAMES = new Set([
27
+ "node_modules",
28
+ ".git",
29
+ "dist",
30
+ "build",
31
+ "target",
32
+ ".next",
33
+ "out",
34
+ "vendor",
35
+ ".cache",
36
+ ]);
37
+
38
+ function vaultIdFromAbsolutePath(absolutePath: string): string {
39
+ return createHash("sha256").update(absolutePath).digest("hex").slice(0, 12);
40
+ }
41
+
42
+ function stripTrailingSlashes(s: string): string {
43
+ let end = s.length;
44
+ while (end > 0 && (s.charAt(end - 1) === "/" || s.charAt(end - 1) === "\\")) end--;
45
+ return s.slice(0, end);
46
+ }
47
+
48
+ function assertWithinVault(vaultRoot: string, relPath: string): string {
49
+ const resolvedRoot = resolve(vaultRoot);
50
+ const candidate = resolve(resolvedRoot, relPath);
51
+ if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + sep)) {
52
+ throw new Error(`Path escapes vault: ${relPath}`);
53
+ }
54
+ return candidate;
55
+ }
56
+
57
+ function loadVaultPaths(): readonly string[] {
58
+ const raw = requireProcessEnv("OBSIDIAN_VAULT_PATHS_JSON");
59
+ let parsed: unknown;
60
+ try {
61
+ parsed = JSON.parse(raw);
62
+ } catch {
63
+ throw new Error("OBSIDIAN_VAULT_PATHS_JSON is not valid JSON");
64
+ }
65
+ if (!Array.isArray(parsed) || parsed.some((p) => typeof p !== "string")) {
66
+ throw new Error("OBSIDIAN_VAULT_PATHS_JSON must be a JSON array of strings");
67
+ }
68
+ return parsed as string[];
69
+ }
70
+
71
+ type VaultEntry = { id: string; root: string; name: string };
72
+
73
+ function discoverVaults(roots: readonly string[]): readonly VaultEntry[] {
74
+ const out: VaultEntry[] = [];
75
+ for (const r of roots) {
76
+ walkForVaults(r, out);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ function walkForVaults(dir: string, out: VaultEntry[]): void {
82
+ let entries: readonly string[];
83
+ try {
84
+ if (!statSync(dir).isDirectory()) return;
85
+ entries = readdirSync(dir);
86
+ } catch {
87
+ return;
88
+ }
89
+ if (entries.includes(VAULT_MARKER)) {
90
+ out.push({
91
+ id: vaultIdFromAbsolutePath(dir),
92
+ root: dir,
93
+ name: basename(stripTrailingSlashes(dir)),
94
+ });
95
+ }
96
+ for (const e of entries) {
97
+ if (e === VAULT_MARKER || DEFAULT_IGNORED_DIR_NAMES.has(e)) continue;
98
+ const sub = join(dir, e);
99
+ let isDir = false;
100
+ try {
101
+ const st = statSync(sub);
102
+ isDir = st.isDirectory() && !st.isSymbolicLink();
103
+ } catch {
104
+ continue;
105
+ }
106
+ if (isDir) walkForVaults(sub, out);
107
+ }
108
+ }
109
+
110
+ function listNotesInVault(vaultRoot: string): readonly string[] {
111
+ const out: string[] = [];
112
+ walkNotes(vaultRoot, vaultRoot, out);
113
+ return out;
114
+ }
115
+
116
+ function walkNotes(currentDir: string, vaultRoot: string, out: string[]): void {
117
+ let entries: readonly string[];
118
+ try {
119
+ if (!statSync(currentDir).isDirectory()) return;
120
+ entries = readdirSync(currentDir);
121
+ } catch {
122
+ return;
123
+ }
124
+ if (currentDir !== vaultRoot && entries.includes(VAULT_MARKER)) return;
125
+ for (const e of entries) {
126
+ if (e === VAULT_MARKER || DEFAULT_IGNORED_DIR_NAMES.has(e)) continue;
127
+ const full = join(currentDir, e);
128
+ let isFile = false;
129
+ let isDir = false;
130
+ try {
131
+ const st = statSync(full);
132
+ if (st.isSymbolicLink()) continue;
133
+ isFile = st.isFile();
134
+ isDir = st.isDirectory();
135
+ } catch {
136
+ continue;
137
+ }
138
+ if (isFile && full.toLowerCase().endsWith(".md")) {
139
+ out.push(relative(vaultRoot, full).replaceAll("\\", "/"));
140
+ } else if (isDir) {
141
+ walkNotes(full, vaultRoot, out);
142
+ }
143
+ }
144
+ }
145
+
146
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
147
+ const H1_RE = /^#\s+(\S.*)$/m;
148
+
149
+ function readNote(
150
+ vaultRoot: string,
151
+ relPath: string,
152
+ ): { title: string; body: string; raw: string } {
153
+ const abs = assertWithinVault(vaultRoot, relPath);
154
+ const raw = readFileSync(abs, "utf8");
155
+ const m = FRONTMATTER_RE.exec(raw);
156
+ const body = m === null ? raw : raw.slice(m[0].length);
157
+ const h1 = H1_RE.exec(body);
158
+ const title =
159
+ h1?.[1]?.trim() !== undefined && h1[1].trim() !== ""
160
+ ? h1[1].trim()
161
+ : basename(relPath).replace(/\.md$/i, "");
162
+ return { title, body, raw };
163
+ }
164
+
165
+ function noteIdFor(vaultId: string, relPath: string): string {
166
+ return `obsidian:${vaultId}#${relPath}`;
167
+ }
168
+
169
+ function findVaultByIdOrPathPrefix(
170
+ vaults: readonly VaultEntry[],
171
+ needle: string,
172
+ ): VaultEntry | undefined {
173
+ return vaults.find((v) => v.id === needle || v.root === needle);
174
+ }
175
+
176
+ const server = new McpServer({ name: "nimbus-obsidian", version: "0.1.0" });
177
+ const registerSimpleTool = createRegisterSimpleTool(server);
178
+
179
+ import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
180
+
181
+ const reg = createZodToolRegistrar(registerSimpleTool);
182
+
183
+ /**
184
+ * Every MUTATING obsidian tool goes through here. Outside the gateway this adds the
185
+ * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
186
+ * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
187
+ */
188
+ const registerWriteTool = createWriteToolRegistrar(server, {
189
+ connector: "obsidian",
190
+ scopeEnv: "NIMBUS_MCP_OBSIDIAN_WRITE_SCOPE",
191
+ scopeKinds: ["vault"],
192
+ });
193
+
194
+ const VAULTS = discoverVaults(loadVaultPaths());
195
+
196
+ const obsidianListSchema = z.object({
197
+ vault: z.string().min(1).optional(),
198
+ tag: z.string().min(1).optional(),
199
+ limit: z.number().int().min(1).max(500).optional(),
200
+ });
201
+
202
+ reg(
203
+ "obsidian_list",
204
+ "List Obsidian notes (optionally filtered by vault id, vault path, or frontmatter tag).",
205
+ obsidianListSchema,
206
+ async (parsed) => {
207
+ const limit = parsed.limit ?? 200;
208
+ const filterVault =
209
+ parsed.vault === undefined ? undefined : findVaultByIdOrPathPrefix(VAULTS, parsed.vault);
210
+ const targetVaults = filterVault === undefined ? VAULTS : [filterVault];
211
+ const out: Array<{
212
+ id: string;
213
+ vault_id: string;
214
+ vault_name: string;
215
+ path: string;
216
+ title: string;
217
+ }> = [];
218
+ for (const v of targetVaults) {
219
+ for (const rel of listNotesInVault(v.root)) {
220
+ if (out.length >= limit) break;
221
+ const { title, raw } = readNote(v.root, rel);
222
+ if (parsed.tag !== undefined) {
223
+ const fm = FRONTMATTER_RE.exec(raw);
224
+ if (!fm?.[1]?.includes(parsed.tag)) continue;
225
+ }
226
+ out.push({
227
+ id: noteIdFor(v.id, rel),
228
+ vault_id: v.id,
229
+ vault_name: v.name,
230
+ path: rel,
231
+ title,
232
+ });
233
+ }
234
+ }
235
+ return jsonResult(out);
236
+ },
237
+ );
238
+
239
+ const obsidianGetSchema = z.object({
240
+ id: z.string().min(1).optional(),
241
+ vault: z.string().min(1).optional(),
242
+ path: z.string().min(1).optional(),
243
+ });
244
+
245
+ reg(
246
+ "obsidian_get",
247
+ "Read a single Obsidian note by id, or by (vault, path) pair.",
248
+ obsidianGetSchema,
249
+ async (parsed) => {
250
+ let v: VaultEntry | undefined;
251
+ let rel = "";
252
+ if (parsed.id !== undefined) {
253
+ const m = /^obsidian:([0-9a-f]{12})#(.+)$/.exec(parsed.id);
254
+ if (m === null) {
255
+ throw new Error(`Invalid obsidian id: ${parsed.id}`);
256
+ }
257
+ v = VAULTS.find((x) => x.id === m[1]);
258
+ rel = m[2] ?? "";
259
+ } else if (parsed.vault !== undefined && parsed.path !== undefined) {
260
+ v = findVaultByIdOrPathPrefix(VAULTS, parsed.vault);
261
+ rel = parsed.path;
262
+ } else {
263
+ throw new Error("obsidian_get requires either `id` or both `vault` and `path`");
264
+ }
265
+ if (v === undefined) {
266
+ throw new Error("Vault not found in OBSIDIAN_VAULT_PATHS_JSON discovery");
267
+ }
268
+ const note = readNote(v.root, rel);
269
+ return jsonResult({
270
+ id: noteIdFor(v.id, rel),
271
+ vault_id: v.id,
272
+ vault_name: v.name,
273
+ path: rel,
274
+ title: note.title,
275
+ body: note.body,
276
+ });
277
+ },
278
+ );
279
+
280
+ const obsidianSearchSchema = z.object({
281
+ query: z.string().min(1),
282
+ vault: z.string().min(1).optional(),
283
+ limit: z.number().int().min(1).max(200).optional(),
284
+ });
285
+
286
+ reg(
287
+ "obsidian_search",
288
+ "Substring-match against note title and body across all configured vaults.",
289
+ obsidianSearchSchema,
290
+ async (parsed) => {
291
+ const limit = parsed.limit ?? 50;
292
+ const needle = parsed.query.toLowerCase();
293
+ const targets =
294
+ parsed.vault === undefined
295
+ ? VAULTS
296
+ : [findVaultByIdOrPathPrefix(VAULTS, parsed.vault)].filter(
297
+ (v): v is VaultEntry => v !== undefined,
298
+ );
299
+ const out: Array<{
300
+ id: string;
301
+ vault_id: string;
302
+ path: string;
303
+ title: string;
304
+ snippet: string;
305
+ }> = [];
306
+ outer: for (const v of targets) {
307
+ for (const rel of listNotesInVault(v.root)) {
308
+ if (out.length >= limit) break outer;
309
+ const note = readNote(v.root, rel);
310
+ const titleHit = note.title.toLowerCase().includes(needle);
311
+ const bodyHitIdx = note.body.toLowerCase().indexOf(needle);
312
+ if (!titleHit && bodyHitIdx < 0) continue;
313
+ const start = Math.max(0, bodyHitIdx - 60);
314
+ const snippet = bodyHitIdx >= 0 ? note.body.slice(start, start + 240) : "";
315
+ out.push({
316
+ id: noteIdFor(v.id, rel),
317
+ vault_id: v.id,
318
+ path: rel,
319
+ title: note.title,
320
+ snippet,
321
+ });
322
+ }
323
+ }
324
+ return jsonResult(out);
325
+ },
326
+ );
327
+
328
+ const appendDailyNoteSchema = z.object({
329
+ vault_id: z.string().min(1),
330
+ content: z.string().min(1),
331
+ date_iso: z
332
+ .string()
333
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
334
+ .optional(),
335
+ });
336
+
337
+ const SUPPORTED_TOKENS = ["YYYY", "YY", "MM", "DD", "HH", "mm"] as const;
338
+
339
+ function pad2(n: number): string {
340
+ return n < 10 ? `0${n}` : String(n);
341
+ }
342
+
343
+ function formatDailyNoteFilename(format: string, date: Date): string {
344
+ const r: Record<string, string> = {
345
+ YYYY: String(date.getUTCFullYear()),
346
+ YY: String(date.getUTCFullYear() % 100).padStart(2, "0"),
347
+ MM: pad2(date.getUTCMonth() + 1),
348
+ DD: pad2(date.getUTCDate()),
349
+ HH: pad2(date.getUTCHours()),
350
+ mm: pad2(date.getUTCMinutes()),
351
+ };
352
+ let out = format;
353
+ for (const tok of SUPPORTED_TOKENS) out = out.replaceAll(tok, r[tok] ?? "");
354
+ return out;
355
+ }
356
+
357
+ function resolveDailyNoteRelativePath(vaultRoot: string, date: Date): string {
358
+ const cfgPath = join(vaultRoot, ".obsidian", "daily-notes.json");
359
+ let folder = "";
360
+ let format = "YYYY-MM-DD";
361
+ try {
362
+ const parsed = JSON.parse(readFileSync(cfgPath, "utf8")) as unknown;
363
+ if (parsed !== null && typeof parsed === "object") {
364
+ const obj = parsed as Record<string, unknown>;
365
+ if (typeof obj["folder"] === "string") folder = obj["folder"];
366
+ if (typeof obj["format"] === "string" && obj["format"] !== "") {
367
+ format = obj["format"];
368
+ }
369
+ }
370
+ } catch {
371
+ // fall through to defaults
372
+ }
373
+ const filename = `${formatDailyNoteFilename(format, date)}.md`;
374
+ return folder === "" ? filename : `${stripTrailingSlashes(folder)}/${filename}`;
375
+ }
376
+
377
+ registerWriteTool(
378
+ "obsidian_append_to_daily_note",
379
+ {
380
+ mutates: "obsidian.note.append",
381
+ // Append-only by construction — it never overwrites — so the note is recoverable by editing.
382
+ recoverable: true,
383
+ scopeTargetOf: (p) => ({ kind: "vault", value: p.vault_id }),
384
+ },
385
+ "Append text to today's Obsidian daily note. Creates the file if it does not exist. Always appends — never overwrites. Adds a leading newline when the existing file does not end in one. Requires HITL `obsidian.note.append`.",
386
+ appendDailyNoteSchema,
387
+ async (parsed) => {
388
+ const v = findVaultByIdOrPathPrefix(VAULTS, parsed.vault_id);
389
+ if (v === undefined) {
390
+ throw new Error("Unknown vault_id");
391
+ }
392
+ const date =
393
+ parsed.date_iso === undefined ? new Date() : new Date(`${parsed.date_iso}T00:00:00Z`);
394
+ const rel = resolveDailyNoteRelativePath(v.root, date);
395
+ const abs = assertWithinVault(v.root, rel);
396
+ mkdirSync(dirname(abs), { recursive: true });
397
+
398
+ const fd = openSync(abs, "a+");
399
+ let bytes = 0;
400
+ try {
401
+ let prefix = "";
402
+ const size = fstatSync(fd).size;
403
+ if (size > 0) {
404
+ const tail = Buffer.alloc(1);
405
+ readSync(fd, tail, 0, 1, size - 1);
406
+ if (tail[0] !== 0x0a) {
407
+ prefix = "\n";
408
+ }
409
+ }
410
+ const final = `${prefix}${parsed.content}`;
411
+ const buf = Buffer.from(final, "utf8");
412
+ writeSync(fd, buf);
413
+ bytes = buf.length;
414
+ } finally {
415
+ try {
416
+ closeSync(fd);
417
+ } catch {
418
+ // ignore close errors
419
+ }
420
+ }
421
+
422
+ return jsonResult({
423
+ appended: true,
424
+ vault_id: v.id,
425
+ vault_name: v.name,
426
+ path: rel,
427
+ bytes,
428
+ });
429
+ },
430
+ );
431
+
432
+ const transport = new StdioServerTransport();
433
+ await server.connect(transport);
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "com.nimbus.onedrive",
3
+ "displayName": "OneDrive",
4
+ "version": "0.1.0",
5
+ "description": "Microsoft Graph OneDrive connector (drive items, search, sharing). Mutations require Gateway HITL.",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": ["graph.microsoft.com", "login.microsoftonline.com"]
11
+ },
12
+ "hitlRequired": ["write", "delete"],
13
+ "syncInterval": 300,
14
+ "minNimbusVersion": "0.2.0"
15
+ }
@@ -0,0 +1,256 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+
5
+ import { resolveUrlWithBase } from "../../../shared/fetch-bearer-json.ts";
6
+ import {
7
+ createRegisterSimpleTool,
8
+ createZodToolRegistrar,
9
+ mcpJsonResult,
10
+ mcpJsonResultIfOk,
11
+ requireProcessEnv,
12
+ type ZodObjectSchema,
13
+ } from "../../../shared/mcp-tool-kit.ts";
14
+ import { makeRestToolRegistrar } from "../../../shared/rest-tool-kit.ts";
15
+
16
+ const GRAPH = "https://graph.microsoft.com/v1.0";
17
+
18
+ async function graphRequest(
19
+ token: string,
20
+ pathOrUrl: string,
21
+ init?: RequestInit,
22
+ ): Promise<{ ok: boolean; status: number; json: unknown; text: string; bytes: number }> {
23
+ const url = resolveUrlWithBase(GRAPH, pathOrUrl);
24
+ const mergedHeaders = new Headers({ Authorization: `Bearer ${token}` });
25
+ if (init?.headers !== undefined) {
26
+ const extra = new Headers(init.headers);
27
+ for (const [k, v] of extra) {
28
+ mergedHeaders.set(k, v);
29
+ }
30
+ }
31
+ const res = await fetch(url, {
32
+ ...init,
33
+ headers: mergedHeaders,
34
+ });
35
+ const buf = await res.arrayBuffer();
36
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(buf);
37
+ let json: unknown;
38
+ try {
39
+ json = JSON.parse(text) as unknown;
40
+ } catch {
41
+ json = null;
42
+ }
43
+ return { ok: res.ok, status: res.status, json, text, bytes: buf.byteLength };
44
+ }
45
+
46
+ const server = new McpServer({ name: "nimbus-onedrive", version: "0.1.0" });
47
+
48
+ import { createWriteToolRegistrar, type WriteToolConfig } from "../../../shared/consent-kit.ts";
49
+
50
+ const reg = createZodToolRegistrar(createRegisterSimpleTool(server));
51
+
52
+ /**
53
+ * Every MUTATING onedrive tool goes through here. Outside the gateway this adds the
54
+ * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
55
+ * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
56
+ */
57
+ const registerWriteTool = createWriteToolRegistrar(server, {
58
+ connector: "onedrive",
59
+ scopeEnv: "NIMBUS_MCP_ONEDRIVE_WRITE_SCOPE",
60
+ scopeKinds: ["item"],
61
+ });
62
+
63
+ /** Standard Graph tool: token → graphRequest(buildPath[, buildInit]) → mcpJsonResultIfOk("Graph", …, 200). */
64
+ /**
65
+ * The write-tool equivalent of `registerOnedriveTool`: identical fetch and result handling, routed
66
+ * through the write registrar.
67
+ */
68
+ function registerOnedriveWriteTool<T>(
69
+ name: string,
70
+ cfg: WriteToolConfig<T>,
71
+ description: string,
72
+ schema: ZodObjectSchema<T>,
73
+ buildPath: (p: T) => string,
74
+ buildInit?: (p: T) => RequestInit,
75
+ ): void {
76
+ registerWriteTool(name, cfg, description, schema, async (parsed) => {
77
+ const token = requireProcessEnv("MICROSOFT_OAUTH_ACCESS_TOKEN");
78
+ const res = await graphRequest(token, buildPath(parsed), buildInit?.(parsed));
79
+ return mcpJsonResultIfOk("Graph", res, 200);
80
+ });
81
+ }
82
+
83
+ const registerOnedriveTool = makeRestToolRegistrar({
84
+ registrar: reg,
85
+ tokenEnv: "MICROSOFT_OAUTH_ACCESS_TOKEN",
86
+ serviceLabel: "Graph",
87
+ fetch: graphRequest,
88
+ snippetMax: 200,
89
+ });
90
+
91
+ const onedriveItemListArgs = z.object({
92
+ parentId: z.string().min(1).optional(),
93
+ pageSize: z.number().int().min(1).max(200).optional(),
94
+ nextLink: z.url().optional(),
95
+ });
96
+
97
+ registerOnedriveTool(
98
+ "onedrive_item_list",
99
+ "List drive items under root or a folder (by parentId). Use nextLink from prior response for pagination.",
100
+ onedriveItemListArgs,
101
+ (data) => {
102
+ if (data.nextLink !== undefined && data.nextLink !== "") {
103
+ return data.nextLink;
104
+ }
105
+ const pageSize = data.pageSize ?? 50;
106
+ if (data.parentId !== undefined && data.parentId !== "") {
107
+ return `/me/drive/items/${encodeURIComponent(data.parentId)}/children?$top=${String(pageSize)}`;
108
+ }
109
+ return `/me/drive/root/children?$top=${String(pageSize)}`;
110
+ },
111
+ );
112
+
113
+ const onedriveItemGetArgs = z.object({
114
+ itemId: z.string().min(1),
115
+ });
116
+
117
+ registerOnedriveTool(
118
+ "onedrive_item_get",
119
+ "Get OneDrive item metadata by id (file or folder).",
120
+ onedriveItemGetArgs,
121
+ (data) => `/me/drive/items/${encodeURIComponent(data.itemId)}`,
122
+ );
123
+
124
+ const onedriveItemDownloadArgs = z.object({
125
+ itemId: z.string().min(1),
126
+ maxBytes: z
127
+ .number()
128
+ .int()
129
+ .min(1024)
130
+ .max(8 * 1024 * 1024)
131
+ .optional(),
132
+ });
133
+
134
+ reg(
135
+ "onedrive_item_download",
136
+ "Download file content as base64 (capped by maxBytes, default 256 KiB). Folders are rejected.",
137
+ onedriveItemDownloadArgs,
138
+ async (data) => {
139
+ const token = requireProcessEnv("MICROSOFT_OAUTH_ACCESS_TOKEN");
140
+ const maxBytes = data.maxBytes ?? 256 * 1024;
141
+ const meta = await graphRequest(
142
+ token,
143
+ `/me/drive/items/${encodeURIComponent(data.itemId)}?$select=id,name,folder,file`,
144
+ );
145
+ if (!meta.ok) {
146
+ throw new Error(`Graph ${String(meta.status)}: ${meta.text.slice(0, 200)}`);
147
+ }
148
+ const m = meta.json;
149
+ if (m === null || typeof m !== "object" || Array.isArray(m)) {
150
+ throw new Error("Invalid metadata response");
151
+ }
152
+ const folder = (m as Record<string, unknown>)["folder"];
153
+ if (folder !== undefined && folder !== null) {
154
+ throw new Error("Item is a folder; download applies to files only");
155
+ }
156
+ const res = await fetch(`${GRAPH}/me/drive/items/${encodeURIComponent(data.itemId)}/content`, {
157
+ headers: { Authorization: `Bearer ${token}` },
158
+ });
159
+ const buf = await res.arrayBuffer();
160
+ if (!res.ok) {
161
+ const err = new TextDecoder().decode(buf);
162
+ throw new Error(`Graph ${String(res.status)}: ${err.slice(0, 200)}`);
163
+ }
164
+ const truncated = buf.byteLength > maxBytes;
165
+ const slice = truncated ? buf.slice(0, maxBytes) : buf;
166
+ const b64 = Buffer.from(slice).toString("base64");
167
+ const out = {
168
+ itemId: data.itemId,
169
+ encoding: "base64" as const,
170
+ truncated,
171
+ byteLength: buf.byteLength,
172
+ returnedBytes: slice.byteLength,
173
+ content: b64,
174
+ };
175
+ return mcpJsonResult(out);
176
+ },
177
+ );
178
+
179
+ const onedriveItemSearchArgs = z.object({
180
+ query: z.string().min(1).max(500),
181
+ pageSize: z.number().int().min(1).max(100).optional(),
182
+ nextLink: z.url().optional(),
183
+ });
184
+
185
+ registerOnedriveTool(
186
+ "onedrive_item_search",
187
+ "Search OneDrive under /me/drive/root/search.",
188
+ onedriveItemSearchArgs,
189
+ (data) => {
190
+ if (data.nextLink !== undefined && data.nextLink !== "") {
191
+ return data.nextLink;
192
+ }
193
+ const pageSize = data.pageSize ?? 25;
194
+ const escaped = data.query.replaceAll("'", "''");
195
+ return `/me/drive/root/search(q='${escaped}')?$top=${String(pageSize)}`;
196
+ },
197
+ );
198
+
199
+ const onedriveItemDeleteArgs = z.object({
200
+ itemId: z.string().min(1),
201
+ });
202
+
203
+ registerWriteTool(
204
+ "onedrive_item_delete",
205
+ {
206
+ mutates: "onedrive.item.delete",
207
+ recoverable: false,
208
+ capturePreState: (p) => Promise.resolve({ itemId: p.itemId }),
209
+ scopeTargetOf: (p) => ({ kind: "item", value: p.itemId }),
210
+ },
211
+ "Permanently delete a OneDrive item. Requires Gateway HITL onedrive.delete.",
212
+ onedriveItemDeleteArgs,
213
+ async (data) => {
214
+ const token = requireProcessEnv("MICROSOFT_OAUTH_ACCESS_TOKEN");
215
+ const r = await graphRequest(token, `/me/drive/items/${encodeURIComponent(data.itemId)}`, {
216
+ method: "DELETE",
217
+ });
218
+ if (!r.ok && r.status !== 204) {
219
+ throw new Error(`Graph ${String(r.status)}: ${r.text.slice(0, 200)}`);
220
+ }
221
+ return mcpJsonResult({ ok: true });
222
+ },
223
+ );
224
+
225
+ const onedriveItemMoveArgs = z.object({
226
+ itemId: z.string().min(1),
227
+ newParentId: z.string().min(1),
228
+ newName: z.string().min(1).max(500).optional(),
229
+ });
230
+
231
+ registerOnedriveWriteTool(
232
+ "onedrive_item_move",
233
+ {
234
+ mutates: "onedrive.item.move",
235
+ recoverable: true,
236
+ scopeTargetOf: (p) => ({ kind: "item", value: p.itemId }),
237
+ },
238
+ "Move (and optionally rename) a drive item. Requires Gateway HITL onedrive.move.",
239
+ onedriveItemMoveArgs,
240
+ (data) => `/me/drive/items/${encodeURIComponent(data.itemId)}`,
241
+ (data) => {
242
+ const body: Record<string, unknown> = {
243
+ parentReference: { id: data.newParentId },
244
+ };
245
+ if (data.newName !== undefined) {
246
+ body["name"] = data.newName;
247
+ }
248
+ return {
249
+ method: "PATCH",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify(body),
252
+ };
253
+ },
254
+ );
255
+
256
+ await server.connect(new StdioServerTransport());