obsidian-mcp-server 1.5.8 → 2.0.2

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 (221) hide show
  1. package/README.md +248 -104
  2. package/dist/config/index.d.ts +41 -0
  3. package/dist/config/index.js +191 -0
  4. package/dist/index.d.ts +1 -5
  5. package/dist/index.js +296 -18
  6. package/dist/mcp-server/server.d.ts +33 -0
  7. package/dist/mcp-server/server.js +211 -0
  8. package/dist/mcp-server/tools/obsidianDeleteFileTool/index.d.ts +12 -0
  9. package/dist/mcp-server/tools/obsidianDeleteFileTool/index.js +12 -0
  10. package/dist/mcp-server/tools/obsidianDeleteFileTool/logic.d.ts +51 -0
  11. package/dist/mcp-server/tools/obsidianDeleteFileTool/logic.js +168 -0
  12. package/dist/mcp-server/tools/obsidianDeleteFileTool/registration.d.ts +19 -0
  13. package/dist/mcp-server/tools/obsidianDeleteFileTool/registration.js +91 -0
  14. package/dist/mcp-server/tools/obsidianGlobalSearchTool/index.d.ts +12 -0
  15. package/dist/mcp-server/tools/obsidianGlobalSearchTool/index.js +12 -0
  16. package/dist/mcp-server/tools/obsidianGlobalSearchTool/logic.d.ts +77 -0
  17. package/dist/mcp-server/tools/obsidianGlobalSearchTool/logic.js +341 -0
  18. package/dist/mcp-server/tools/obsidianGlobalSearchTool/registration.d.ts +18 -0
  19. package/dist/mcp-server/tools/obsidianGlobalSearchTool/registration.js +69 -0
  20. package/dist/mcp-server/tools/obsidianListFilesTool/index.d.ts +12 -0
  21. package/dist/mcp-server/tools/obsidianListFilesTool/index.js +12 -0
  22. package/dist/mcp-server/tools/obsidianListFilesTool/logic.d.ts +64 -0
  23. package/dist/mcp-server/tools/obsidianListFilesTool/logic.js +179 -0
  24. package/dist/mcp-server/tools/obsidianListFilesTool/registration.d.ts +19 -0
  25. package/dist/mcp-server/tools/obsidianListFilesTool/registration.js +96 -0
  26. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/index.d.ts +3 -0
  27. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/index.js +2 -0
  28. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/logic.d.ts +42 -0
  29. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/logic.js +152 -0
  30. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/registration.d.ts +3 -0
  31. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/registration.js +52 -0
  32. package/dist/mcp-server/tools/obsidianManageTagsTool/index.d.ts +3 -0
  33. package/dist/mcp-server/tools/obsidianManageTagsTool/index.js +2 -0
  34. package/dist/mcp-server/tools/obsidianManageTagsTool/logic.d.ts +28 -0
  35. package/dist/mcp-server/tools/obsidianManageTagsTool/logic.js +161 -0
  36. package/dist/mcp-server/tools/obsidianManageTagsTool/registration.d.ts +3 -0
  37. package/dist/mcp-server/tools/obsidianManageTagsTool/registration.js +52 -0
  38. package/dist/mcp-server/tools/obsidianReadFileTool/index.d.ts +12 -0
  39. package/dist/mcp-server/tools/obsidianReadFileTool/index.js +12 -0
  40. package/dist/mcp-server/tools/obsidianReadFileTool/logic.d.ts +87 -0
  41. package/dist/mcp-server/tools/obsidianReadFileTool/logic.js +216 -0
  42. package/dist/mcp-server/tools/obsidianReadFileTool/registration.d.ts +20 -0
  43. package/dist/mcp-server/tools/obsidianReadFileTool/registration.js +101 -0
  44. package/dist/mcp-server/tools/obsidianSearchReplaceTool/index.d.ts +12 -0
  45. package/dist/mcp-server/tools/obsidianSearchReplaceTool/index.js +12 -0
  46. package/dist/mcp-server/tools/obsidianSearchReplaceTool/logic.d.ts +255 -0
  47. package/dist/mcp-server/tools/obsidianSearchReplaceTool/logic.js +583 -0
  48. package/dist/mcp-server/tools/obsidianSearchReplaceTool/registration.d.ts +22 -0
  49. package/dist/mcp-server/tools/obsidianSearchReplaceTool/registration.js +111 -0
  50. package/dist/mcp-server/tools/obsidianUpdateFileTool/index.d.ts +12 -0
  51. package/dist/mcp-server/tools/obsidianUpdateFileTool/index.js +12 -0
  52. package/dist/mcp-server/tools/obsidianUpdateFileTool/logic.d.ts +183 -0
  53. package/dist/mcp-server/tools/obsidianUpdateFileTool/logic.js +490 -0
  54. package/dist/mcp-server/tools/obsidianUpdateFileTool/registration.d.ts +21 -0
  55. package/dist/mcp-server/tools/obsidianUpdateFileTool/registration.js +108 -0
  56. package/dist/mcp-server/transports/authentication/authContext.d.ts +33 -0
  57. package/dist/mcp-server/transports/authentication/authContext.js +24 -0
  58. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +30 -0
  59. package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
  60. package/dist/mcp-server/transports/authentication/authUtils.d.ts +18 -0
  61. package/dist/mcp-server/transports/authentication/authUtils.js +45 -0
  62. package/dist/mcp-server/transports/authentication/oauthMiddleware.d.ts +24 -0
  63. package/dist/mcp-server/transports/authentication/oauthMiddleware.js +109 -0
  64. package/dist/mcp-server/transports/authentication/types.d.ts +17 -0
  65. package/dist/mcp-server/transports/authentication/types.js +5 -0
  66. package/dist/mcp-server/transports/httpTransport.d.ts +24 -0
  67. package/dist/mcp-server/transports/httpTransport.js +496 -0
  68. package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
  69. package/dist/mcp-server/transports/stdioTransport.js +63 -0
  70. package/dist/services/obsidianRestAPI/index.d.ts +15 -0
  71. package/dist/services/obsidianRestAPI/index.js +17 -0
  72. package/dist/services/obsidianRestAPI/methods/activeFileMethods.d.ts +38 -0
  73. package/dist/services/obsidianRestAPI/methods/activeFileMethods.js +62 -0
  74. package/dist/services/obsidianRestAPI/methods/commandMethods.d.ts +22 -0
  75. package/dist/services/obsidianRestAPI/methods/commandMethods.js +31 -0
  76. package/dist/services/obsidianRestAPI/methods/openMethods.d.ts +16 -0
  77. package/dist/services/obsidianRestAPI/methods/openMethods.js +21 -0
  78. package/dist/services/obsidianRestAPI/methods/patchMethods.d.ts +37 -0
  79. package/dist/services/obsidianRestAPI/methods/patchMethods.js +94 -0
  80. package/dist/services/obsidianRestAPI/methods/periodicNoteMethods.d.ts +42 -0
  81. package/dist/services/obsidianRestAPI/methods/periodicNoteMethods.js +66 -0
  82. package/dist/services/obsidianRestAPI/methods/searchMethods.d.ts +25 -0
  83. package/dist/services/obsidianRestAPI/methods/searchMethods.js +36 -0
  84. package/dist/services/obsidianRestAPI/methods/vaultMethods.d.ts +58 -0
  85. package/dist/services/obsidianRestAPI/methods/vaultMethods.js +144 -0
  86. package/dist/services/obsidianRestAPI/service.d.ts +195 -0
  87. package/dist/services/obsidianRestAPI/service.js +379 -0
  88. package/dist/services/obsidianRestAPI/types.d.ts +127 -0
  89. package/dist/services/obsidianRestAPI/types.js +7 -0
  90. package/dist/services/obsidianRestAPI/vaultCache/index.d.ts +4 -0
  91. package/dist/services/obsidianRestAPI/vaultCache/index.js +4 -0
  92. package/dist/services/obsidianRestAPI/vaultCache/service.d.ts +88 -0
  93. package/dist/services/obsidianRestAPI/vaultCache/service.js +299 -0
  94. package/dist/types-global/errors.d.ts +73 -0
  95. package/dist/types-global/errors.js +71 -0
  96. package/dist/utils/index.d.ts +5 -8
  97. package/dist/utils/index.js +13 -9
  98. package/dist/utils/internal/asyncUtils.d.ts +54 -0
  99. package/dist/utils/internal/asyncUtils.js +101 -0
  100. package/dist/utils/internal/errorHandler.d.ts +176 -0
  101. package/dist/utils/internal/errorHandler.js +351 -0
  102. package/dist/utils/internal/index.d.ts +4 -0
  103. package/dist/utils/internal/index.js +4 -0
  104. package/dist/utils/internal/logger.d.ts +141 -0
  105. package/dist/utils/internal/logger.js +406 -0
  106. package/dist/utils/internal/requestContext.d.ts +83 -0
  107. package/dist/utils/internal/requestContext.js +72 -0
  108. package/dist/utils/metrics/index.d.ts +1 -0
  109. package/dist/utils/metrics/index.js +1 -0
  110. package/dist/utils/metrics/tokenCounter.d.ts +27 -0
  111. package/dist/utils/metrics/tokenCounter.js +128 -0
  112. package/dist/utils/obsidian/index.d.ts +5 -0
  113. package/dist/utils/obsidian/index.js +5 -0
  114. package/dist/utils/obsidian/obsidianApiUtils.d.ts +14 -0
  115. package/dist/utils/obsidian/obsidianApiUtils.js +29 -0
  116. package/dist/utils/obsidian/obsidianStatUtils.d.ts +68 -0
  117. package/dist/utils/obsidian/obsidianStatUtils.js +143 -0
  118. package/dist/utils/parsing/dateParser.d.ts +56 -0
  119. package/dist/utils/parsing/dateParser.js +104 -0
  120. package/dist/utils/parsing/index.d.ts +2 -0
  121. package/dist/utils/parsing/index.js +3 -0
  122. package/dist/utils/parsing/jsonParser.d.ts +80 -0
  123. package/dist/utils/parsing/jsonParser.js +133 -0
  124. package/dist/utils/security/idGenerator.d.ts +140 -0
  125. package/dist/utils/security/idGenerator.js +194 -0
  126. package/dist/utils/security/index.d.ts +3 -0
  127. package/dist/utils/security/index.js +3 -0
  128. package/dist/utils/security/rateLimiter.d.ts +156 -0
  129. package/dist/utils/security/rateLimiter.js +235 -0
  130. package/dist/utils/security/sanitization.d.ts +244 -0
  131. package/dist/utils/security/sanitization.js +599 -0
  132. package/package.json +58 -37
  133. package/dist/index.js.map +0 -1
  134. package/dist/mcp/handlers.d.ts +0 -29
  135. package/dist/mcp/handlers.js +0 -305
  136. package/dist/mcp/handlers.js.map +0 -1
  137. package/dist/mcp/index.d.ts +0 -6
  138. package/dist/mcp/index.js +0 -7
  139. package/dist/mcp/index.js.map +0 -1
  140. package/dist/mcp/server.d.ts +0 -18
  141. package/dist/mcp/server.js +0 -240
  142. package/dist/mcp/server.js.map +0 -1
  143. package/dist/mcp/types.d.ts +0 -70
  144. package/dist/mcp/types.js +0 -49
  145. package/dist/mcp/types.js.map +0 -1
  146. package/dist/obsidian/client.d.ts +0 -109
  147. package/dist/obsidian/client.js +0 -403
  148. package/dist/obsidian/client.js.map +0 -1
  149. package/dist/obsidian/errors.d.ts +0 -28
  150. package/dist/obsidian/errors.js +0 -75
  151. package/dist/obsidian/errors.js.map +0 -1
  152. package/dist/obsidian/index.d.ts +0 -6
  153. package/dist/obsidian/index.js +0 -7
  154. package/dist/obsidian/index.js.map +0 -1
  155. package/dist/obsidian/types.d.ts +0 -107
  156. package/dist/obsidian/types.js +0 -12
  157. package/dist/obsidian/types.js.map +0 -1
  158. package/dist/resources/index.d.ts +0 -13
  159. package/dist/resources/index.js +0 -15
  160. package/dist/resources/index.js.map +0 -1
  161. package/dist/resources/tags.d.ts +0 -39
  162. package/dist/resources/tags.js +0 -257
  163. package/dist/resources/tags.js.map +0 -1
  164. package/dist/resources/types.d.ts +0 -27
  165. package/dist/resources/types.js +0 -5
  166. package/dist/resources/types.js.map +0 -1
  167. package/dist/tools/base.d.ts +0 -46
  168. package/dist/tools/base.js +0 -88
  169. package/dist/tools/base.js.map +0 -1
  170. package/dist/tools/files/content.d.ts +0 -58
  171. package/dist/tools/files/content.js +0 -171
  172. package/dist/tools/files/content.js.map +0 -1
  173. package/dist/tools/files/index.d.ts +0 -14
  174. package/dist/tools/files/index.js +0 -22
  175. package/dist/tools/files/index.js.map +0 -1
  176. package/dist/tools/files/list.d.ts +0 -35
  177. package/dist/tools/files/list.js +0 -133
  178. package/dist/tools/files/list.js.map +0 -1
  179. package/dist/tools/index.d.ts +0 -21
  180. package/dist/tools/index.js +0 -31
  181. package/dist/tools/index.js.map +0 -1
  182. package/dist/tools/properties/index.d.ts +0 -14
  183. package/dist/tools/properties/index.js +0 -19
  184. package/dist/tools/properties/index.js.map +0 -1
  185. package/dist/tools/properties/manager.d.ts +0 -62
  186. package/dist/tools/properties/manager.js +0 -302
  187. package/dist/tools/properties/manager.js.map +0 -1
  188. package/dist/tools/properties/tools.d.ts +0 -47
  189. package/dist/tools/properties/tools.js +0 -239
  190. package/dist/tools/properties/tools.js.map +0 -1
  191. package/dist/tools/properties/types.d.ts +0 -141
  192. package/dist/tools/properties/types.js +0 -70
  193. package/dist/tools/properties/types.js.map +0 -1
  194. package/dist/tools/search/complex.d.ts +0 -38
  195. package/dist/tools/search/complex.js +0 -270
  196. package/dist/tools/search/complex.js.map +0 -1
  197. package/dist/tools/search/index.d.ts +0 -14
  198. package/dist/tools/search/index.js +0 -20
  199. package/dist/tools/search/index.js.map +0 -1
  200. package/dist/tools/search/simple.d.ts +0 -25
  201. package/dist/tools/search/simple.js +0 -127
  202. package/dist/tools/search/simple.js.map +0 -1
  203. package/dist/utils/errors.d.ts +0 -24
  204. package/dist/utils/errors.js +0 -59
  205. package/dist/utils/errors.js.map +0 -1
  206. package/dist/utils/idGenerator.d.ts +0 -15
  207. package/dist/utils/idGenerator.js +0 -21
  208. package/dist/utils/idGenerator.js.map +0 -1
  209. package/dist/utils/index.js.map +0 -1
  210. package/dist/utils/logging.d.ts +0 -245
  211. package/dist/utils/logging.js +0 -417
  212. package/dist/utils/logging.js.map +0 -1
  213. package/dist/utils/rate-limiting.d.ts +0 -50
  214. package/dist/utils/rate-limiting.js +0 -94
  215. package/dist/utils/rate-limiting.js.map +0 -1
  216. package/dist/utils/tokenization.d.ts +0 -28
  217. package/dist/utils/tokenization.js +0 -75
  218. package/dist/utils/tokenization.js.map +0 -1
  219. package/dist/utils/validation.d.ts +0 -22
  220. package/dist/utils/validation.js +0 -92
  221. package/dist/utils/validation.js.map +0 -1
@@ -0,0 +1,496 @@
1
+ /**
2
+ * @fileoverview Handles the setup and management of the Streamable HTTP MCP transport using Hono.
3
+ * Implements the MCP Specification 2025-03-26 for Streamable HTTP.
4
+ * This includes creating a Hono server, configuring middleware (CORS, Authentication),
5
+ * defining request routing for the single MCP endpoint (POST/GET/DELETE),
6
+ * managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
7
+ * and binding to a network port with retry logic for port conflicts.
8
+ *
9
+ * Specification Reference:
10
+ * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
11
+ * @module src/mcp-server/transports/httpTransport
12
+ */
13
+ import { serve } from "@hono/node-server";
14
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
15
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
16
+ import { Hono } from "hono";
17
+ import { cors } from "hono/cors";
18
+ import http from "http";
19
+ import { randomUUID } from "node:crypto";
20
+ import { config } from "../../config/index.js";
21
+ import { BaseErrorCode, McpError } from "../../types-global/errors.js";
22
+ import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
23
+ import { mcpAuthMiddleware } from "./authentication/authMiddleware.js";
24
+ import { oauthMiddleware } from "./authentication/oauthMiddleware.js";
25
+ /**
26
+ * The port number for the HTTP transport, configured via `MCP_HTTP_PORT` environment variable.
27
+ * Defaults to 3010 if not specified (default is managed by the config module).
28
+ * @constant {number} HTTP_PORT
29
+ * @private
30
+ */
31
+ const HTTP_PORT = config.mcpHttpPort;
32
+ /**
33
+ * The host address for the HTTP transport, configured via `MCP_HTTP_HOST` environment variable.
34
+ * Defaults to '127.0.0.1' if not specified (default is managed by the config module).
35
+ * MCP Spec Security Note: Recommends binding to localhost for local servers to minimize exposure.
36
+ * @private
37
+ */
38
+ const HTTP_HOST = config.mcpHttpHost;
39
+ /**
40
+ * The single HTTP endpoint path for all MCP communication, as required by the MCP specification.
41
+ * This endpoint supports POST, GET, DELETE, and OPTIONS methods.
42
+ * @constant {string} MCP_ENDPOINT_PATH
43
+ * @private
44
+ */
45
+ const MCP_ENDPOINT_PATH = "/mcp";
46
+ /**
47
+ * Maximum number of attempts to find an available port if the initial `HTTP_PORT` is in use.
48
+ * The server will try ports sequentially: `HTTP_PORT`, `HTTP_PORT + 1`, ..., up to `MAX_PORT_RETRIES`.
49
+ * @constant {number} MAX_PORT_RETRIES
50
+ * @private
51
+ */
52
+ const MAX_PORT_RETRIES = 15;
53
+ /**
54
+ * Stores active `StreamableHTTPServerTransport` instances from the SDK, keyed by their session ID.
55
+ * This is essential for routing subsequent HTTP requests (GET, DELETE, non-initialize POST)
56
+ * to the correct stateful session transport instance.
57
+ * @type {Record<string, StreamableHTTPServerTransport>}
58
+ * @private
59
+ */
60
+ const httpTransports = {};
61
+ /**
62
+ * Stores the last activity timestamp for each session, keyed by session ID.
63
+ * Used for garbage collecting stale/abandoned sessions.
64
+ * @type {Record<string, number>}
65
+ * @private
66
+ */
67
+ const sessionActivity = {};
68
+ /**
69
+ * The timeout period in milliseconds for inactive sessions. If a session has no
70
+ * activity for this duration, it will be considered stale and garbage collected.
71
+ * Defaults to 30 minutes.
72
+ * @constant {number} SESSION_TIMEOUT_MS
73
+ * @private
74
+ */
75
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
76
+ /**
77
+ * The interval in milliseconds at which the session garbage collector runs to
78
+ * clean up stale sessions. Defaults to 1 minute.
79
+ * @constant {number} SESSION_GC_INTERVAL_MS
80
+ * @private
81
+ */
82
+ const SESSION_GC_INTERVAL_MS = 60 * 1000; // 1 minute
83
+ /**
84
+ * Proactively checks if a specific network port is already in use.
85
+ * @param port - The port number to check.
86
+ * @param host - The host address to check the port on.
87
+ * @param parentContext - Logging context from the caller.
88
+ * @returns A promise that resolves to `true` if the port is in use, or `false` otherwise.
89
+ * @private
90
+ */
91
+ async function isPortInUse(port, host, parentContext) {
92
+ const checkContext = requestContextService.createRequestContext({
93
+ ...parentContext,
94
+ operation: "isPortInUse",
95
+ port,
96
+ host,
97
+ });
98
+ logger.debug(`Proactively checking port usability...`, checkContext);
99
+ return new Promise((resolve) => {
100
+ const tempServer = http.createServer();
101
+ tempServer
102
+ .once("error", (err) => {
103
+ if (err.code === "EADDRINUSE") {
104
+ logger.debug(`Proactive check: Port confirmed in use (EADDRINUSE).`, checkContext);
105
+ resolve(true);
106
+ }
107
+ else {
108
+ logger.debug(`Proactive check: Non-EADDRINUSE error encountered: ${err.message}`, { ...checkContext, errorCode: err.code });
109
+ resolve(false);
110
+ }
111
+ })
112
+ .once("listening", () => {
113
+ logger.debug(`Proactive check: Port is available.`, checkContext);
114
+ tempServer.close(() => resolve(false));
115
+ })
116
+ .listen(port, host);
117
+ });
118
+ }
119
+ /**
120
+ * Attempts to start the HTTP server, retrying on incrementing ports if `EADDRINUSE` occurs.
121
+ *
122
+ * @param app - The Hono application instance.
123
+ * @param initialPort - The initial port number to try.
124
+ * @param host - The host address to bind to.
125
+ * @param maxRetries - Maximum number of additional ports to attempt.
126
+ * @param parentContext - Logging context from the caller.
127
+ * @returns A promise that resolves with the Node.js `http.Server` instance the server successfully bound to.
128
+ * @throws {Error} If binding fails after all retries or for a non-EADDRINUSE error.
129
+ * @private
130
+ */
131
+ function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
132
+ const startContext = requestContextService.createRequestContext({
133
+ ...parentContext,
134
+ operation: "startHttpServerWithRetry",
135
+ initialPort,
136
+ host,
137
+ maxRetries,
138
+ });
139
+ logger.debug(`Attempting to start HTTP server...`, startContext);
140
+ return new Promise(async (resolve, reject) => {
141
+ let lastError = null;
142
+ for (let i = 0; i <= maxRetries; i++) {
143
+ const currentPort = initialPort + i;
144
+ const attemptContext = requestContextService.createRequestContext({
145
+ ...startContext,
146
+ port: currentPort,
147
+ attempt: i + 1,
148
+ maxAttempts: maxRetries + 1,
149
+ });
150
+ logger.debug(`Attempting port ${currentPort} (${attemptContext.attempt}/${attemptContext.maxAttempts})`, attemptContext);
151
+ if (await isPortInUse(currentPort, host, attemptContext)) {
152
+ logger.warning(`Proactive check detected port ${currentPort} is in use, retrying...`, attemptContext);
153
+ lastError = new Error(`EADDRINUSE: Port ${currentPort} detected as in use by proactive check.`);
154
+ await new Promise((res) => setTimeout(res, 100));
155
+ continue;
156
+ }
157
+ try {
158
+ const serverInstance = serve({ fetch: app.fetch, port: currentPort, hostname: host }, (info) => {
159
+ const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
160
+ logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
161
+ // Display user-friendly startup message only after server is confirmed listening
162
+ let serverAddressLog = serverAddress;
163
+ let productionNote = "";
164
+ if (config.environment === "production") {
165
+ serverAddressLog = `https://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
166
+ productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
167
+ }
168
+ if (process.stdout.isTTY) {
169
+ console.log(`\nšŸš€ MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
170
+ }
171
+ });
172
+ resolve(serverInstance);
173
+ return;
174
+ }
175
+ catch (err) {
176
+ lastError = err;
177
+ logger.debug(`Listen error on port ${currentPort}: Code=${err.code}, Message=${err.message}`, { ...attemptContext, errorCode: err.code, errorMessage: err.message });
178
+ if (err.code === "EADDRINUSE") {
179
+ logger.warning(`Port ${currentPort} already in use (EADDRINUSE), retrying...`, attemptContext);
180
+ await new Promise((res) => setTimeout(res, 100));
181
+ }
182
+ else {
183
+ logger.error(`Failed to bind to port ${currentPort} due to non-EADDRINUSE error: ${err.message}`, { ...attemptContext, error: err.message });
184
+ reject(err);
185
+ return;
186
+ }
187
+ }
188
+ }
189
+ logger.error(`Failed to bind to any port after ${maxRetries + 1} attempts. Last error: ${lastError?.message}`, { ...startContext, error: lastError?.message });
190
+ reject(lastError ||
191
+ new Error("Failed to bind to any port after multiple retries."));
192
+ });
193
+ }
194
+ /**
195
+ * Sets up and starts the Streamable HTTP transport layer for the MCP server.
196
+ *
197
+ * @param createServerInstanceFn - An asynchronous factory function that returns a new `McpServer` instance.
198
+ * @param parentContext - Logging context from the main server startup process.
199
+ * @returns A promise that resolves with the Node.js `http.Server` instance when the HTTP server is successfully listening.
200
+ * @throws {Error} If the server fails to start after all port retries.
201
+ */
202
+ export async function startHttpTransport(createServerInstanceFn, parentContext) {
203
+ const app = new Hono();
204
+ const transportContext = requestContextService.createRequestContext({
205
+ ...parentContext,
206
+ transportType: "HTTP",
207
+ component: "HttpTransportSetup",
208
+ });
209
+ logger.debug("Setting up Hono app for HTTP transport...", transportContext);
210
+ // Start the session garbage collector
211
+ setInterval(() => {
212
+ const now = Date.now();
213
+ const gcContext = requestContextService.createRequestContext({
214
+ operation: "SessionGarbageCollector",
215
+ });
216
+ logger.debug("Running session garbage collector...", gcContext);
217
+ for (const sessionId in sessionActivity) {
218
+ if (now - sessionActivity[sessionId] > SESSION_TIMEOUT_MS) {
219
+ logger.info(`Session ${sessionId} timed out due to inactivity. Cleaning up.`, { ...gcContext, sessionId });
220
+ const transport = httpTransports[sessionId];
221
+ if (transport) {
222
+ transport.close(); // This will trigger the onclose handler to delete it from httpTransports
223
+ }
224
+ delete sessionActivity[sessionId];
225
+ }
226
+ }
227
+ }, SESSION_GC_INTERVAL_MS);
228
+ app.use("*", cors({
229
+ origin: config.mcpAllowedOrigins || [],
230
+ allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
231
+ allowHeaders: [
232
+ "Content-Type",
233
+ "Mcp-Session-Id",
234
+ "Last-Event-ID",
235
+ "Authorization",
236
+ ],
237
+ credentials: true,
238
+ }));
239
+ app.use("*", async (c, next) => {
240
+ const securityContext = requestContextService.createRequestContext({
241
+ ...transportContext,
242
+ operation: "securityMiddleware",
243
+ path: c.req.path,
244
+ method: c.req.method,
245
+ origin: c.req.header("origin"),
246
+ });
247
+ logger.debug(`Applying security middleware...`, securityContext);
248
+ c.res.headers.set("X-Content-Type-Options", "nosniff");
249
+ c.res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
250
+ c.res.headers.set("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self'; connect-src 'self'");
251
+ logger.debug("Security middleware passed.", securityContext);
252
+ await next();
253
+ });
254
+ app.use(MCP_ENDPOINT_PATH, async (c, next) => {
255
+ const xff = c.req.header("x-forwarded-for");
256
+ const clientIp = xff ? xff.split(",")[0].trim() : "unknown_ip";
257
+ const rateLimitKey = clientIp || c.req.header("host") || "unknown_ip_for_rate_limit";
258
+ const context = requestContextService.createRequestContext({
259
+ operation: "httpRateLimitCheck",
260
+ ipAddress: rateLimitKey,
261
+ method: c.req.method,
262
+ path: c.req.path,
263
+ });
264
+ try {
265
+ rateLimiter.check(rateLimitKey, context);
266
+ logger.debug("Rate limit check passed.", context);
267
+ await next();
268
+ }
269
+ catch (error) {
270
+ if (error instanceof McpError &&
271
+ error.code === BaseErrorCode.RATE_LIMITED) {
272
+ logger.warning(`Rate limit exceeded for IP: ${rateLimitKey}`, {
273
+ ...context,
274
+ errorMessage: error.message,
275
+ details: error.details,
276
+ });
277
+ return c.json({
278
+ jsonrpc: "2.0",
279
+ error: { code: -32000, message: "Too Many Requests" },
280
+ id: (await c.req.json().catch(() => ({})))?.id || null,
281
+ }, 429);
282
+ }
283
+ else {
284
+ logger.error("Unexpected error in rate limit middleware", {
285
+ ...context,
286
+ error: error instanceof Error ? error.message : String(error),
287
+ });
288
+ throw error;
289
+ }
290
+ }
291
+ });
292
+ // Use the appropriate authentication middleware based on config
293
+ if (config.mcpAuthMode === "oauth") {
294
+ app.use(MCP_ENDPOINT_PATH, oauthMiddleware);
295
+ }
296
+ else {
297
+ app.use(MCP_ENDPOINT_PATH, mcpAuthMiddleware);
298
+ }
299
+ app.post(MCP_ENDPOINT_PATH, async (c) => {
300
+ const basePostContext = requestContextService.createRequestContext({
301
+ ...transportContext,
302
+ operation: "handlePost",
303
+ method: "POST",
304
+ path: c.req.path,
305
+ origin: c.req.header("origin"),
306
+ });
307
+ const body = await c.req.json();
308
+ logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, {
309
+ ...basePostContext,
310
+ headers: c.req.header(),
311
+ bodyPreview: JSON.stringify(body).substring(0, 100),
312
+ });
313
+ const sessionId = c.req.header("mcp-session-id");
314
+ logger.debug(`Extracted session ID: ${sessionId}`, {
315
+ ...basePostContext,
316
+ sessionId,
317
+ });
318
+ let transport = sessionId ? httpTransports[sessionId] : undefined;
319
+ if (transport && sessionId) {
320
+ sessionActivity[sessionId] = Date.now(); // Update activity timestamp
321
+ }
322
+ logger.debug(`Found existing transport for session ID: ${!!transport}`, {
323
+ ...basePostContext,
324
+ sessionId,
325
+ });
326
+ const isInitReq = isInitializeRequest(body);
327
+ logger.debug(`Is InitializeRequest: ${isInitReq}`, {
328
+ ...basePostContext,
329
+ sessionId,
330
+ });
331
+ const requestId = body?.id || null;
332
+ try {
333
+ if (isInitReq) {
334
+ if (transport) {
335
+ logger.warning("Received InitializeRequest on an existing session ID. Closing old session and creating new.", { ...basePostContext, sessionId });
336
+ await transport.close();
337
+ // onclose handler will delete from httpTransports and sessionActivity
338
+ }
339
+ logger.info("Handling Initialize Request: Creating new session...", {
340
+ ...basePostContext,
341
+ sessionId,
342
+ });
343
+ transport = new StreamableHTTPServerTransport({
344
+ sessionIdGenerator: () => {
345
+ const newId = randomUUID();
346
+ logger.debug(`Generated new session ID: ${newId}`, basePostContext);
347
+ return newId;
348
+ },
349
+ onsessioninitialized: (newId) => {
350
+ logger.debug(`Session initialized callback triggered for ID: ${newId}`, { ...basePostContext, newSessionId: newId });
351
+ httpTransports[newId] = transport;
352
+ sessionActivity[newId] = Date.now(); // Initialize activity timestamp
353
+ logger.info(`HTTP Session created: ${newId}`, {
354
+ ...basePostContext,
355
+ newSessionId: newId,
356
+ });
357
+ },
358
+ });
359
+ transport.onclose = () => {
360
+ const closedSessionId = transport.sessionId;
361
+ if (closedSessionId) {
362
+ logger.debug(`onclose handler triggered for session ID: ${closedSessionId}`, { ...basePostContext, closedSessionId });
363
+ delete httpTransports[closedSessionId];
364
+ delete sessionActivity[closedSessionId]; // Clean up activity tracker
365
+ logger.info(`HTTP Session closed: ${closedSessionId}`, {
366
+ ...basePostContext,
367
+ closedSessionId,
368
+ });
369
+ }
370
+ else {
371
+ logger.debug("onclose handler triggered for transport without session ID (likely init failure).", basePostContext);
372
+ }
373
+ };
374
+ logger.debug("Creating McpServer instance for new session...", basePostContext);
375
+ const server = await createServerInstanceFn();
376
+ logger.debug("Connecting McpServer to new transport...", basePostContext);
377
+ await server.connect(transport);
378
+ logger.debug("McpServer connected to transport.", basePostContext);
379
+ }
380
+ else if (!transport) {
381
+ logger.warning("Invalid or missing session ID for non-initialize POST request.", { ...basePostContext, sessionId });
382
+ return c.json({
383
+ jsonrpc: "2.0",
384
+ error: { code: -32004, message: "Invalid or expired session ID" },
385
+ id: requestId,
386
+ }, 404);
387
+ }
388
+ const currentSessionId = transport.sessionId;
389
+ logger.debug(`Processing POST request content for session ${currentSessionId}...`, { ...basePostContext, sessionId: currentSessionId, isInitReq });
390
+ const response = await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
391
+ logger.debug(`Finished processing POST request content for session ${currentSessionId}.`, { ...basePostContext, sessionId: currentSessionId });
392
+ return response;
393
+ }
394
+ catch (err) {
395
+ const errorSessionId = transport?.sessionId || sessionId;
396
+ logger.error("Error handling POST request", {
397
+ ...basePostContext,
398
+ sessionId: errorSessionId,
399
+ isInitReq,
400
+ error: err instanceof Error ? err.message : String(err),
401
+ stack: err instanceof Error ? err.stack : undefined,
402
+ });
403
+ if (isInitReq && transport && !transport.sessionId) {
404
+ logger.debug("Cleaning up transport after initialization failure.", {
405
+ ...basePostContext,
406
+ sessionId: errorSessionId,
407
+ });
408
+ await transport.close().catch((closeErr) => logger.error("Error closing transport after init failure", {
409
+ ...basePostContext,
410
+ sessionId: errorSessionId,
411
+ closeError: closeErr,
412
+ }));
413
+ }
414
+ return c.json({
415
+ jsonrpc: "2.0",
416
+ error: {
417
+ code: -32603,
418
+ message: "Internal server error during POST handling",
419
+ },
420
+ id: requestId,
421
+ }, 500);
422
+ }
423
+ });
424
+ const handleSessionReq = async (c) => {
425
+ const method = c.req.method;
426
+ const baseSessionReqContext = requestContextService.createRequestContext({
427
+ ...transportContext,
428
+ operation: `handle${method}`,
429
+ method,
430
+ path: c.req.path,
431
+ origin: c.req.header("origin"),
432
+ });
433
+ logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, {
434
+ ...baseSessionReqContext,
435
+ headers: c.req.header(),
436
+ });
437
+ const sessionId = c.req.header("mcp-session-id");
438
+ logger.debug(`Extracted session ID: ${sessionId}`, {
439
+ ...baseSessionReqContext,
440
+ sessionId,
441
+ });
442
+ const transport = sessionId ? httpTransports[sessionId] : undefined;
443
+ if (transport && sessionId) {
444
+ sessionActivity[sessionId] = Date.now(); // Update activity timestamp
445
+ }
446
+ logger.debug(`Found existing transport for session ID: ${!!transport}`, {
447
+ ...baseSessionReqContext,
448
+ sessionId,
449
+ });
450
+ if (!transport) {
451
+ logger.warning(`Session not found for ${method} request`, {
452
+ ...baseSessionReqContext,
453
+ sessionId,
454
+ });
455
+ return c.json({
456
+ jsonrpc: "2.0",
457
+ error: { code: -32004, message: "Session not found or expired" },
458
+ id: null,
459
+ }, 404);
460
+ }
461
+ try {
462
+ logger.debug(`Delegating ${method} request to transport for session ${sessionId}...`, { ...baseSessionReqContext, sessionId });
463
+ const response = await transport.handleRequest(c.env.incoming, c.env.outgoing);
464
+ logger.info(`Successfully handled ${method} request for session ${sessionId}`, { ...baseSessionReqContext, sessionId });
465
+ return response;
466
+ }
467
+ catch (err) {
468
+ logger.error(`Error handling ${method} request for session ${sessionId}`, {
469
+ ...baseSessionReqContext,
470
+ sessionId,
471
+ error: err instanceof Error ? err.message : String(err),
472
+ stack: err instanceof Error ? err.stack : undefined,
473
+ });
474
+ return c.json({
475
+ jsonrpc: "2.0",
476
+ error: { code: -32603, message: "Internal Server Error" },
477
+ id: null,
478
+ }, 500);
479
+ }
480
+ };
481
+ app.get(MCP_ENDPOINT_PATH, handleSessionReq);
482
+ app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
483
+ logger.debug("Creating HTTP server instance...", transportContext);
484
+ try {
485
+ logger.debug("Attempting to start HTTP server with retry logic...", transportContext);
486
+ const serverInstance = await startHttpServerWithRetry(app, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
487
+ return serverInstance;
488
+ }
489
+ catch (err) {
490
+ logger.fatal("HTTP server failed to start after multiple port retries.", {
491
+ ...transportContext,
492
+ error: err instanceof Error ? err.message : String(err),
493
+ });
494
+ throw err;
495
+ }
496
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @fileoverview Handles the setup and connection for the Stdio MCP transport.
3
+ * Implements the MCP Specification 2025-03-26 for stdio transport.
4
+ * This transport communicates directly over standard input (stdin) and
5
+ * standard output (stdout), typically used when the MCP server is launched
6
+ * as a child process by a host application.
7
+ *
8
+ * Specification Reference:
9
+ * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#stdio
10
+ *
11
+ * --- Authentication Note ---
12
+ * As per the MCP Authorization Specification (2025-03-26, Section 1.2),
13
+ * STDIO transports SHOULD NOT implement HTTP-based authentication flows.
14
+ * Authorization is typically handled implicitly by the host application
15
+ * controlling the server process. This implementation follows that guideline.
16
+ *
17
+ * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
18
+ * @module src/mcp-server/transports/stdioTransport
19
+ */
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ import { RequestContext } from "../../utils/index.js";
22
+ /**
23
+ * Connects a given `McpServer` instance to the Stdio transport.
24
+ * This function initializes the SDK's `StdioServerTransport`, which manages
25
+ * communication over `process.stdin` and `process.stdout` according to the
26
+ * MCP stdio transport specification.
27
+ *
28
+ * MCP Spec Points Covered by SDK's `StdioServerTransport`:
29
+ * - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
30
+ * - Writes JSON-RPC messages to stdout.
31
+ * - Handles newline delimiters and ensures no embedded newlines in output messages.
32
+ * - Ensures only valid MCP messages are written to stdout.
33
+ *
34
+ * Logging via the `logger` utility MAY result in output to stderr, which is
35
+ * permitted by the spec for logging purposes.
36
+ *
37
+ * @param server - The `McpServer` instance.
38
+ * @param parentContext - The logging and tracing context from the calling function.
39
+ * @returns A promise that resolves when the Stdio transport is successfully connected.
40
+ * @throws {Error} If the connection fails during setup.
41
+ */
42
+ export declare function connectStdioTransport(server: McpServer, parentContext: RequestContext): Promise<void>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @fileoverview Handles the setup and connection for the Stdio MCP transport.
3
+ * Implements the MCP Specification 2025-03-26 for stdio transport.
4
+ * This transport communicates directly over standard input (stdin) and
5
+ * standard output (stdout), typically used when the MCP server is launched
6
+ * as a child process by a host application.
7
+ *
8
+ * Specification Reference:
9
+ * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#stdio
10
+ *
11
+ * --- Authentication Note ---
12
+ * As per the MCP Authorization Specification (2025-03-26, Section 1.2),
13
+ * STDIO transports SHOULD NOT implement HTTP-based authentication flows.
14
+ * Authorization is typically handled implicitly by the host application
15
+ * controlling the server process. This implementation follows that guideline.
16
+ *
17
+ * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
18
+ * @module src/mcp-server/transports/stdioTransport
19
+ */
20
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
+ import { ErrorHandler, logger } from "../../utils/index.js";
22
+ /**
23
+ * Connects a given `McpServer` instance to the Stdio transport.
24
+ * This function initializes the SDK's `StdioServerTransport`, which manages
25
+ * communication over `process.stdin` and `process.stdout` according to the
26
+ * MCP stdio transport specification.
27
+ *
28
+ * MCP Spec Points Covered by SDK's `StdioServerTransport`:
29
+ * - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
30
+ * - Writes JSON-RPC messages to stdout.
31
+ * - Handles newline delimiters and ensures no embedded newlines in output messages.
32
+ * - Ensures only valid MCP messages are written to stdout.
33
+ *
34
+ * Logging via the `logger` utility MAY result in output to stderr, which is
35
+ * permitted by the spec for logging purposes.
36
+ *
37
+ * @param server - The `McpServer` instance.
38
+ * @param parentContext - The logging and tracing context from the calling function.
39
+ * @returns A promise that resolves when the Stdio transport is successfully connected.
40
+ * @throws {Error} If the connection fails during setup.
41
+ */
42
+ export async function connectStdioTransport(server, parentContext) {
43
+ const operationContext = {
44
+ ...parentContext,
45
+ operation: "connectStdioTransport",
46
+ transportType: "Stdio",
47
+ };
48
+ logger.debug("Attempting to connect stdio transport...", operationContext);
49
+ try {
50
+ logger.debug("Creating StdioServerTransport instance...", operationContext);
51
+ const transport = new StdioServerTransport();
52
+ logger.debug("Connecting McpServer instance to StdioServerTransport...", operationContext);
53
+ await server.connect(transport);
54
+ logger.info("MCP Server connected and listening via stdio transport.", operationContext);
55
+ if (process.stdout.isTTY) {
56
+ console.log(`\nšŸš€ MCP Server running in STDIO mode.\n (MCP Spec: 2025-03-26 Stdio Transport)\n`);
57
+ }
58
+ }
59
+ catch (err) {
60
+ ErrorHandler.handleError(err, { ...operationContext, critical: true });
61
+ throw err; // Re-throw after handling to allow caller to react if necessary
62
+ }
63
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @module ObsidianRestApiService Barrel File
3
+ * @description
4
+ * Exports the singleton instance of the Obsidian REST API service and related types.
5
+ */
6
+ export * from "./types.js";
7
+ export { ObsidianRestApiService } from "./service.js";
8
+ export * as activeFileMethods from "./methods/activeFileMethods.js";
9
+ export * as commandMethods from "./methods/commandMethods.js";
10
+ export * as openMethods from "./methods/openMethods.js";
11
+ export * as patchMethods from "./methods/patchMethods.js";
12
+ export * as periodicNoteMethods from "./methods/periodicNoteMethods.js";
13
+ export * as searchMethods from "./methods/searchMethods.js";
14
+ export * as vaultMethods from "./methods/vaultMethods.js";
15
+ export { VaultCacheService } from "./vaultCache/index.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @module ObsidianRestApiService Barrel File
3
+ * @description
4
+ * Exports the singleton instance of the Obsidian REST API service and related types.
5
+ */
6
+ export * from "./types.js"; // Export all types
7
+ // Removed singleton export
8
+ export { ObsidianRestApiService } from "./service.js"; // Export the class itself
9
+ // Export method modules if direct access is desired, though typically accessed via service instance
10
+ export * as activeFileMethods from "./methods/activeFileMethods.js";
11
+ export * as commandMethods from "./methods/commandMethods.js";
12
+ export * as openMethods from "./methods/openMethods.js";
13
+ export * as patchMethods from "./methods/patchMethods.js";
14
+ export * as periodicNoteMethods from "./methods/periodicNoteMethods.js";
15
+ export * as searchMethods from "./methods/searchMethods.js";
16
+ export * as vaultMethods from "./methods/vaultMethods.js";
17
+ export { VaultCacheService } from "./vaultCache/index.js";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @module ActiveFileMethods
3
+ * @description
4
+ * Methods for interacting with the currently active file in Obsidian via the REST API.
5
+ */
6
+ import { RequestContext } from "../../../utils/index.js";
7
+ import { NoteJson, RequestFunction } from "../types.js";
8
+ /**
9
+ * Gets the content of the currently active file in Obsidian.
10
+ * @param _request - The internal request function from the service instance.
11
+ * @param format - 'markdown' or 'json' (for NoteJson).
12
+ * @param context - Request context.
13
+ * @returns The file content (string) or NoteJson object.
14
+ */
15
+ export declare function getActiveFile(_request: RequestFunction, format: "markdown" | "json" | undefined, context: RequestContext): Promise<string | NoteJson>;
16
+ /**
17
+ * Updates (overwrites) the content of the currently active file.
18
+ * @param _request - The internal request function from the service instance.
19
+ * @param content - The new content.
20
+ * @param context - Request context.
21
+ * @returns {Promise<void>} Resolves on success (204 No Content).
22
+ */
23
+ export declare function updateActiveFile(_request: RequestFunction, content: string, context: RequestContext): Promise<void>;
24
+ /**
25
+ * Appends content to the end of the currently active file.
26
+ * @param _request - The internal request function from the service instance.
27
+ * @param content - The content to append.
28
+ * @param context - Request context.
29
+ * @returns {Promise<void>} Resolves on success (204 No Content).
30
+ */
31
+ export declare function appendActiveFile(_request: RequestFunction, content: string, context: RequestContext): Promise<void>;
32
+ /**
33
+ * Deletes the currently active file.
34
+ * @param _request - The internal request function from the service instance.
35
+ * @param context - Request context.
36
+ * @returns {Promise<void>} Resolves on success (204 No Content).
37
+ */
38
+ export declare function deleteActiveFile(_request: RequestFunction, context: RequestContext): Promise<void>;