Repository navigation
feat(observability): add Obsidian context source with 24 MCP tools - #3077
diegosouzapw merged 8 commits into
Conversation
…to all dynamic tool definitions - Move !enforceScopes guard before MCP_TOOL_MAP lookup in evaluateToolScopes() - Add inlineScopes parameter for dynamic tool scope resolution - Add scopes to all 33 dynamic tool definitions across 5 tool files - Wire toolDef.scopes through withScopeEnforcement in server.ts - Preserves existing behavior: tool_definition_missing returned when enforceScopes=true and no scopes found anywhere
fix: evaluateToolScopes guard order and add scopes to all dynamic MCP tools
feat: Notion MCP context source (search, read, query, write Notion via routed models)
There was a problem hiding this comment.
Code Review
This pull request introduces Notion as an MCP context source, adding six new tools, a REST API client, token persistence, settings endpoints, and a dashboard UI card, alongside fixing scope enforcement for dynamic tool groups. The review feedback highlights several critical and high-severity issues: the token persistence function incorrectly uses INSERT OR IGNORE which prevents token updates; the scope enforcement logic blocks tools that require no scopes; the settings API saves the token before validating it, risking overwriting a valid token; the Notion API client leaks timeout resources and misclassifies aborted requests; the block-appending tool silently truncates payloads instead of enforcing the 100-block limit via Zod; the documentation contains discrepancies regarding tool names; and the new UI card hardcodes English strings instead of utilizing the instantiated translation hook.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function setNotionToken(token: string): void { | ||
| try { | ||
| const db = getDbInstance(); | ||
| db.prepare( | ||
| "INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES (?, ?, ?)" | ||
| ).run(NOTION_NAMESPACE, NOTION_TOKEN_KEY, JSON.stringify(token)); | ||
| } catch { | ||
| // Non-fatal — token still works in-memory if persistence fails. | ||
| } | ||
| } |
There was a problem hiding this comment.
Using INSERT OR IGNORE prevents updating the Notion token if one already exists in the database. When a user changes their token in the settings UI, the database will silently ignore the insert and keep the old token. Use INSERT OR REPLACE to allow updating the token correctly.
| export function setNotionToken(token: string): void { | |
| try { | |
| const db = getDbInstance(); | |
| db.prepare( | |
| "INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES (?, ?, ?)" | |
| ).run(NOTION_NAMESPACE, NOTION_TOKEN_KEY, JSON.stringify(token)); | |
| } catch { | |
| // Non-fatal — token still works in-memory if persistence fails. | |
| } | |
| } | |
| export function setNotionToken(token: string): void { | |
| try { | |
| const db = getDbInstance(); | |
| db.prepare( | |
| "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)" | |
| ).run(NOTION_NAMESPACE, NOTION_TOKEN_KEY, JSON.stringify(token)); | |
| } catch { | |
| // Non-fatal. | |
| } | |
| } |
| const toolScopes = inlineScopes ?? MCP_TOOL_MAP[toolName]?.scopes; | ||
| const required = Array.isArray(toolScopes) ? Array.from(toolScopes) : []; | ||
|
|
||
| if (required.length === 0) { | ||
| return { | ||
| allowed: false, | ||
| required: [], | ||
| provided: Array.from(callerScopes), | ||
| provided, | ||
| missing: [], | ||
| reason: "tool_definition_missing", | ||
| }; | ||
| } |
There was a problem hiding this comment.
If a tool requires no scopes (i.e., scopes is an empty array []), required.length will be 0. Under the current logic, this returns allowed: false with reason: "tool_definition_missing", which blocks any public or scope-free tools. We should check if the tool definition actually exists to distinguish between a missing tool and a tool that requires no scopes.
const toolDef = MCP_TOOL_MAP[toolName];
const toolScopes = inlineScopes ?? toolDef?.scopes;
if (!inlineScopes && !toolDef) {
return {
allowed: false,
required: [],
provided,
missing: [],
reason: "tool_definition_missing",
};
}
const required = Array.isArray(toolScopes) ? Array.from(toolScopes) : [];
if (!enforceScopes || required.length === 0) {
return { allowed: true, required, provided, missing: [] };
}| try { | ||
| setNotionToken(parsed.data.token); | ||
|
|
||
| const client = createNotionClient(parsed.data.token); | ||
| const result = await client.searchPagesAndDatabases("test", undefined, 1); | ||
| if (result && typeof result === "object" && "object" in result && (result as Record<string, unknown>).object === "error") { | ||
| clearNotionToken(); | ||
| return NextResponse.json( | ||
| { error: "Token validation failed: invalid token", connected: false }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| return NextResponse.json({ | ||
| connected: true, | ||
| message: "Notion integration token saved and validated", | ||
| }); | ||
| } catch (error) { | ||
| clearNotionToken(); | ||
| const msg = error instanceof Error ? error.message : String(error); | ||
| return NextResponse.json({ error: msg, connected: false }, { status: 400 }); | ||
| } |
There was a problem hiding this comment.
The POST handler saves the token to the database before validating it, and deletes it if validation fails. This means if the user had a valid token and entered an invalid one, the existing valid token is lost. It is much safer to validate the token first, and only save it to the database if validation succeeds.
try {
const client = createNotionClient(parsed.data.token);
const result = await client.searchPagesAndDatabases("test", undefined, 1);
if (result && typeof result === "object" && "object" in result && (result as Record<string, unknown>).object === "error") {
return NextResponse.json(
{ error: "Token validation failed: invalid token", connected: false },
{ status: 400 }
);
}
setNotionToken(parsed.data.token);
return NextResponse.json({
connected: true,
message: "Notion integration token saved and validated",
});
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return NextResponse.json({ error: msg, connected: false }, { status: 400 });
}| for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { | ||
| try { | ||
| const response = await fetch(url, { | ||
| ...options, | ||
| headers: { | ||
| Authorization: `Bearer ${apiKey}`, | ||
| "Notion-Version": NOTION_VERSION, | ||
| "Content-Type": "application/json", | ||
| ...(options.headers as Record<string, string>), | ||
| }, | ||
| signal: mergedSignal, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const body = await response.json().catch(() => ({})) as Record<string, unknown>; | ||
| const errBody = body as Partial<NotionErrorBody>; | ||
| const code = errBody?.code ?? "unknown"; | ||
| const msg = errBody?.message ?? `HTTP ${response.status}`; | ||
| const error = classifyNotionError(response.status, code, msg); | ||
|
|
||
| if (error instanceof NotionRateLimitError) { | ||
| lastError = error; | ||
| const waitMs = error.retryAfter * 1000 + Math.pow(2, attempt) * 200; | ||
| await sleep(waitMs); | ||
| continue; | ||
| } | ||
|
|
||
| if (error instanceof NotionServerError && attempt < MAX_RETRIES - 1) { | ||
| lastError = error; | ||
| await sleep(Math.pow(2, attempt) * 500); | ||
| continue; | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
|
|
||
| return response.json(); | ||
| } catch (err) { | ||
| if (err instanceof Error && err.name === "AbortError") { | ||
| clearTimeout(timeout); | ||
| throw new NotionTimeoutError("Notion API request timed out after 55s"); | ||
| } | ||
| if (err instanceof NotionAuthError || err instanceof NotionNotFoundError || err instanceof NotionValidationError) { | ||
| clearTimeout(timeout); | ||
| throw err; | ||
| } | ||
| if (attempt < MAX_RETRIES - 1) { | ||
| lastError = err instanceof Error ? err : new NotionServerError(String(err)); | ||
| await sleep(Math.pow(2, attempt) * 500); | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| clearTimeout(timeout); | ||
| throw lastError ?? new NotionServerError("Exhausted all retries"); |
There was a problem hiding this comment.
There are two issues here:
- Resource Leak:
clearTimeout(timeout)is not called when the fetch succeeds and returnsresponse.json(). This leaks the timeout in the event loop. - AbortError Misclassification: If the caller aborts the request via
options.signal, it throws anAbortError, which is caught and re-thrown as aNotionTimeoutErrorsaying "Notion API request timed out after 55s". This is incorrect and misleading.
Wrapping the loop in a try...finally block ensures the timeout is always cleared, and checking controller.signal.aborted allows distinguishing between a timeout and a caller abort.
try {
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${apiKey}`,
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
...(options.headers as Record<string, string>),
},
signal: mergedSignal,
});
if (!response.ok) {
const body = await response.json().catch(() => ({})) as Record<string, unknown>;
const errBody = body as Partial<NotionErrorBody>;
const code = errBody?.code ?? "unknown";
const msg = errBody?.message ?? `HTTP ${response.status}`;
const error = classifyNotionError(response.status, code, msg);
if (error instanceof NotionRateLimitError) {
lastError = error;
const waitMs = error.retryAfter * 1000 + Math.pow(2, attempt) * 200;
await sleep(waitMs);
continue;
}
if (error instanceof NotionServerError && attempt < MAX_RETRIES - 1) {
lastError = error;
await sleep(Math.pow(2, attempt) * 500);
continue;
}
throw error;
}
return await response.json();
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
if (controller.signal.aborted) {
throw new NotionTimeoutError("Notion API request timed out after 55s");
}
throw err;
}
if (err instanceof NotionAuthError || err instanceof NotionNotFoundError || err instanceof NotionValidationError) {
throw err;
}
if (attempt < MAX_RETRIES - 1) {
lastError = err instanceof Error ? err : new NotionServerError(String(err));
await sleep(Math.pow(2, attempt) * 500);
continue;
}
}
}
throw lastError ?? new NotionServerError("Exhausted all retries");
} finally {
clearTimeout(timeout);
}| blockId: z.string().min(1).describe("Target block or page ID to append to"), | ||
| children: z.array(z.unknown()).describe("Array of block objects to append"), | ||
| after: z.string().optional().describe("Block ID to append after (position parameter)"), | ||
| }), | ||
| handler: async (args: { blockId: string; children: unknown[]; after?: string }) => { |
There was a problem hiding this comment.
The Notion API has a strict limit of 100 blocks per request when appending blocks. In src/lib/notion/api.ts, the array is silently sliced to 100 elements, which can lead to silent data loss/truncation. We should enforce this limit in the Zod schema so that the caller gets a clear validation error if they exceed 100 blocks.
inputSchema: z.object({
blockId: z.string().min(1).describe("Target block or page ID to append to"),
children: z.array(z.unknown()).max(100).describe("Array of block objects to append (max 100)"),
after: z.string().optional().describe("Block ID to append after (position parameter)"),
}),| | Tool | Scopes | Description | | ||
| | :--------------------------- | :--------------- | :------------------------------------------------------------------------------------ | | ||
| | `omniroute_notion_search` | `read:notion` | Full-text search across all pages and databases | | ||
| | `omniroute_notion_list_databases` | `read:notion` | List all accessible databases with schema metadata | | ||
| | `omniroute_notion_get_database` | `read:notion` | Get database schema by ID | | ||
| | `omniroute_notion_query_database` | `read:notion` | Query a database with filters, sorts, and pagination | | ||
| | `omniroute_notion_read` | `read:notion` | Read a page or block by ID with its content | | ||
| | `omniroute_notion_append_blocks` | `write:notion`| Append children blocks to a parent block (max 100 per request) | |
There was a problem hiding this comment.
The documentation table for the Notion Context Source has a few discrepancies:
- It lists
omniroute_notion_list_databasesandomniroute_notion_read, but these tools do not exist innotionTools.ts. Instead,notionTools.tsdefinesnotion_get_pageandnotion_list_block_children. - The tool names in the table are prefixed with
omniroute_(e.g.,omniroute_notion_search), but they are registered inserver.tsusing their raw names (e.g.,notion_search).
| | Tool | Scopes | Description | | |
| | :--------------------------- | :--------------- | :------------------------------------------------------------------------------------ | | |
| | `omniroute_notion_search` | `read:notion` | Full-text search across all pages and databases | | |
| | `omniroute_notion_list_databases` | `read:notion` | List all accessible databases with schema metadata | | |
| | `omniroute_notion_get_database` | `read:notion` | Get database schema by ID | | |
| | `omniroute_notion_query_database` | `read:notion` | Query a database with filters, sorts, and pagination | | |
| | `omniroute_notion_read` | `read:notion` | Read a page or block by ID with its content | | |
| | `omniroute_notion_append_blocks` | `write:notion`| Append children blocks to a parent block (max 100 per request) | | |
| | Tool | Scopes | Description | | |
| | :--------------------------- | :--------------- | :------------------------------------------------------------------------------------ | | |
| | `notion_search` | `read:notion` | Full-text search across all pages and databases | | |
| | `notion_get_page` | `read:notion` | Get the content and metadata of a Notion page by its ID | | |
| | `notion_list_block_children` | `read:notion` | List all block children of a Notion block or page | | |
| | `notion_get_database` | `read:notion` | Get metadata and schema of a Notion database by its ID | | |
| | `notion_query_database` | `read:notion` | Query a Notion database with filters, sorts, and pagination | | |
| | `notion_append_blocks` | `write:notion` | Append block children to an existing Notion block or page (max 100 per request) | |
| <div className="flex items-center gap-2 flex-wrap"> | ||
| <span className="font-semibold text-sm">Notion</span> | ||
| <Badge variant={connected ? "success" : "default"}> | ||
| {connected ? "Connected" : "Not connected"} | ||
| </Badge> | ||
| </div> | ||
| <p className="text-xs text-text-muted mt-0.5"> | ||
| Search, read, query, and write to Notion through routed AI models | ||
| </p> | ||
| </div> |
There was a problem hiding this comment.
The component instantiates const t = useTranslations("endpoint"); but does not use it for any of the user-facing strings. All strings are hardcoded in English, violating the project's internationalization standards. Use the t translation function for these strings.
| <div className="flex items-center gap-2 flex-wrap"> | |
| <span className="font-semibold text-sm">Notion</span> | |
| <Badge variant={connected ? "success" : "default"}> | |
| {connected ? "Connected" : "Not connected"} | |
| </Badge> | |
| </div> | |
| <p className="text-xs text-text-muted mt-0.5"> | |
| Search, read, query, and write to Notion through routed AI models | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2 flex-wrap"> | |
| <span className="font-semibold text-sm">Notion</span> | |
| <Badge variant={connected ? "success" : "default"}> | |
| {connected ? t("connected") : t("notConnected")} | |
| </Badge> | |
| </div> | |
| <p className="text-xs text-text-muted mt-0.5"> | |
| {t("notionDescription")} | |
| </p> | |
| </div> |
Adds Obsidian as a context source via the Local REST API community plugin. 24 MCP tools scoped under read:obsidian / write:obsidian covering search, read, write, vault management, and bidirectional sync. - open-sse/mcp-server/tools/obsidianTools.ts — 24 tool definitions - src/lib/obsidian/api.ts — REST client with retry, timeout, error classification - src/lib/db/obsidian.ts — token/URL/vault_path persistence in key_value table - src/lib/obsidianSync.ts — WebDAV sync helpers - src/app/api/settings/obsidian/route.ts — GET/POST/DELETE settings API - ObsidianSourceCard.tsx — dashboard UI card - obsidian-plugin/ — community plugin for desktop↔mobile vault sync - 6 test files (unit + integration), 52+ tests Fixes: INSERT OR IGNORE → INSERT OR REPLACE in setObsidianToken() to allow token updates. Uses relative paths in install-obsidian-plugin.sh. Co-Authored-By: Claude <noreply@anthropic.com>
…a removal - Integrates 24 Obsidian MCP tools (read/write/search/sync) via Local REST API plugin - Adds read:obsidian and write:obsidian scopes - Adds DB modules for Obsidian and Notion configuration - Adds REST endpoints: /api/settings/obsidian and /api/settings/notion - Adds ObsidianSourceCard.tsx and NotionSourceCard.tsx UI components - Removes .serena/ from repo tracking (added to .gitignore — local-only AI tool config) - Resolves conflicts with release/v3.8.9: keeps agentSkillTools registration + adds obsidianTools
- Add skillRegistry/skillExecutor imports + define skillToMcpToolName inline - Remove missing apiKeyContextSources import in db/obsidian.ts (stub per-key fallback) - Add missing getDbInstance import in src/lib/obsidian/api.ts
9db306e
into
cheaper-inference:release/v3.8.9
|
Thank you @branben for this excellent addition! The Obsidian context source with 24 MCP tools follows the same pattern as the Notion integration and is a great addition to OmniRoute. Adjustments applied before merging: (1) removed .serena/ AI tool config from the repo (added to .gitignore), (2) resolved import conflicts with the release branch while preserving agentSkillTools, (3) fixed missing module imports (getDbInstance in obsidian/api.ts, stub for apiKeyContextSources). Merged into release/v3.8.9 — will ship in v3.8.9! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9892e3f9eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // ── Notion Context Source Tools ─────────────── | ||
| notionTools.forEach((toolDef) => { | ||
| server.registerTool( |
There was a problem hiding this comment.
Remove duplicate Notion tool registration
When createMcpServer() runs, this second notionTools.forEach registers every Notion tool name after the earlier Notion block at lines 1145-1170 already registered the same names. The MCP SDK rejects duplicate tool names during registration, so starting the MCP server now fails before any tools (including the new Obsidian tools) are usable.
Useful? React with 👍 / 👎.
| import { useTranslations } from "next-intl"; | ||
| import { Card, Button, Input, Badge } from "@/shared/components"; | ||
|
|
||
| export default function ObsidianSourceCard() { |
There was a problem hiding this comment.
Mount the Obsidian card in Context Sources
The new settings UI is never reachable: a repo-wide rg "ObsidianSourceCard" only finds this component definition, while EndpointPageClient.tsx still imports/renders only NotionSourceCard in the Context Sources tab. In the documented Settings > Context Sources flow, users therefore cannot configure the Obsidian token from the UI and would have to call the API manually.
Useful? React with 👍 / 👎.
| const res = await fetch("/api/settings/obsidian/webdav", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ vaultPath: vaultPath.trim() }), |
There was a problem hiding this comment.
Add the WebDAV settings route before calling it
When the WebDAV Enable button is used, this POST targets /api/settings/obsidian/webdav, but a repo-wide rg "settings/obsidian/webdav|api/v1/webdav" src/app open-sse src/lib shows only these client calls and helper code, with no Next route handler for the endpoint. The request will hit a 404 instead of calling enableObsidianVaultSync, so the advertised vault sync setup cannot be enabled from the UI.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9892e3f9eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // ── Notion Context Source Tools ─────────────── | ||
| notionTools.forEach((toolDef) => { | ||
| server.registerTool( |
There was a problem hiding this comment.
Remove duplicate Notion tool registration
When createMcpServer() runs, this second notionTools.forEach registers every Notion tool name after the earlier Notion block at lines 1145-1170 already registered the same names. The MCP SDK rejects duplicate tool names during registration, so starting the MCP server now fails before any tools (including the new Obsidian tools) are usable.
Useful? React with 👍 / 👎.
| import { useTranslations } from "next-intl"; | ||
| import { Card, Button, Input, Badge } from "@/shared/components"; | ||
|
|
||
| export default function ObsidianSourceCard() { |
There was a problem hiding this comment.
Mount the Obsidian card in Context Sources
The new settings UI is never reachable: a repo-wide rg "ObsidianSourceCard" only finds this component definition, while EndpointPageClient.tsx still imports/renders only NotionSourceCard in the Context Sources tab. In the documented Settings > Context Sources flow, users therefore cannot configure the Obsidian token from the UI and would have to call the API manually.
Useful? React with 👍 / 👎.
| const res = await fetch("/api/settings/obsidian/webdav", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ vaultPath: vaultPath.trim() }), |
There was a problem hiding this comment.
Add the WebDAV settings route before calling it
When the WebDAV Enable button is used, this POST targets /api/settings/obsidian/webdav, but a repo-wide rg "settings/obsidian/webdav|api/v1/webdav" src/app open-sse src/lib shows only these client calls and helper code, with no Next route handler for the endpoint. The request will hit a 404 instead of calling enableObsidianVaultSync, so the advertised vault sync setup cannot be enabled from the UI.
Useful? React with 👍 / 👎.
….ts (dynamic tools) The Lint job's check:any-budget:t11 (string-blind /\bany\b/ regex) failed on: - open-sse/executors/cursor.ts: the WORD 'any' in #3104's tool-commit/output- constraint prompt strings — zero real TS `any` in the file. - open-sse/mcp-server/server.ts: 3 `(toolDef: any)` in dynamic memory/skill/ compression tool-registration loops (#3077), guarded by existing @ts-ignore. Both are v3.8.9-introduced and benign; set the per-file baseline to the count.
…drift + #3100 dedup) Full CI surfaced real failures that local subsets missed (gh-merged PRs bypass the hooks that run these gates): - typecheck:core (Lint job): 3 now-unused @ts-expect-error in mcp-server/server.ts (#3077 dynamic tool loops) → @ts-ignore (lenient, no TS2578). - pack-artifact-policy.test.ts: build-reorg (#3124) renamed app/->dist/; the test still asserted app/ paths + REQUIRED order (it sorts alphabetically). - electron-packaging.test.ts: extraResources from .next/electron-standalone -> .build/electron-standalone (#3124). - glm-provider-model-import-route.test.ts: two GLM connections shared one apiKey, so #3100 (#3023) dedup collapsed them → only one discovery fetch. Distinct keys. Remaining CI flakes (batch expiration, ModelSync self-fetch) pass in isolation — concurrency/port flakiness under --test-concurrency=4, not real failures.
…plugin e2e - hasStandaloneAppBundle now accepts the legacy app/ bundle too (mirrors serve CLI's dist/->app/ fallback), fixing postinstall-support.test.ts after #3124. - obsidian-plugin-e2e: #3077 committed the e2e test but NEVER committed its dependency obsidian-plugin/src/server.ts (un-ignored but unstaged) nor the 'obsidian' npm pkg, so it crashed with ERR_MODULE_NOT_FOUND on every fresh checkout. Load the runtime values dynamically and skip the suite when absent (unit sync logic stays covered by obsidian-plugin-sync.test.ts).
…heaper-inference#3077) Integrated into release/v3.8.9. Fixes applied: removed .serena/ from repo (gitignored), resolved import conflicts preserving agentSkillTools, fixed TS errors.
….ts (dynamic tools) The Lint job's check:any-budget:t11 (string-blind /\bany\b/ regex) failed on: - open-sse/executors/cursor.ts: the WORD 'any' in cheaper-inference#3104's tool-commit/output- constraint prompt strings — zero real TS `any` in the file. - open-sse/mcp-server/server.ts: 3 `(toolDef: any)` in dynamic memory/skill/ compression tool-registration loops (cheaper-inference#3077), guarded by existing @ts-ignore. Both are v3.8.9-introduced and benign; set the per-file baseline to the count.
…drift + cheaper-inference#3100 dedup) Full CI surfaced real failures that local subsets missed (gh-merged PRs bypass the hooks that run these gates): - typecheck:core (Lint job): 3 now-unused @ts-expect-error in mcp-server/server.ts (cheaper-inference#3077 dynamic tool loops) → @ts-ignore (lenient, no TS2578). - pack-artifact-policy.test.ts: build-reorg (cheaper-inference#3124) renamed app/->dist/; the test still asserted app/ paths + REQUIRED order (it sorts alphabetically). - electron-packaging.test.ts: extraResources from .next/electron-standalone -> .build/electron-standalone (cheaper-inference#3124). - glm-provider-model-import-route.test.ts: two GLM connections shared one apiKey, so cheaper-inference#3100 (cheaper-inference#3023) dedup collapsed them → only one discovery fetch. Distinct keys. Remaining CI flakes (batch expiration, ModelSync self-fetch) pass in isolation — concurrency/port flakiness under --test-concurrency=4, not real failures.
…plugin e2e - hasStandaloneAppBundle now accepts the legacy app/ bundle too (mirrors serve CLI's dist/->app/ fallback), fixing postinstall-support.test.ts after cheaper-inference#3124. - obsidian-plugin-e2e: cheaper-inference#3077 committed the e2e test but NEVER committed its dependency obsidian-plugin/src/server.ts (un-ignored but unstaged) nor the 'obsidian' npm pkg, so it crashed with ERR_MODULE_NOT_FOUND on every fresh checkout. Load the runtime values dynamically and skip the suite when absent (unit sync logic stays covered by obsidian-plugin-sync.test.ts).
…heaper-inference#3077) Integrated into release/v3.8.9. Fixes applied: removed .serena/ from repo (gitignored), resolved import conflicts preserving agentSkillTools, fixed TS errors.
….ts (dynamic tools) The Lint job's check:any-budget:t11 (string-blind /\bany\b/ regex) failed on: - open-sse/executors/cursor.ts: the WORD 'any' in cheaper-inference#3104's tool-commit/output- constraint prompt strings — zero real TS `any` in the file. - open-sse/mcp-server/server.ts: 3 `(toolDef: any)` in dynamic memory/skill/ compression tool-registration loops (cheaper-inference#3077), guarded by existing @ts-ignore. Both are v3.8.9-introduced and benign; set the per-file baseline to the count.
…drift + cheaper-inference#3100 dedup) Full CI surfaced real failures that local subsets missed (gh-merged PRs bypass the hooks that run these gates): - typecheck:core (Lint job): 3 now-unused @ts-expect-error in mcp-server/server.ts (cheaper-inference#3077 dynamic tool loops) → @ts-ignore (lenient, no TS2578). - pack-artifact-policy.test.ts: build-reorg (cheaper-inference#3124) renamed app/->dist/; the test still asserted app/ paths + REQUIRED order (it sorts alphabetically). - electron-packaging.test.ts: extraResources from .next/electron-standalone -> .build/electron-standalone (cheaper-inference#3124). - glm-provider-model-import-route.test.ts: two GLM connections shared one apiKey, so cheaper-inference#3100 (cheaper-inference#3023) dedup collapsed them → only one discovery fetch. Distinct keys. Remaining CI flakes (batch expiration, ModelSync self-fetch) pass in isolation — concurrency/port flakiness under --test-concurrency=4, not real failures.
…plugin e2e - hasStandaloneAppBundle now accepts the legacy app/ bundle too (mirrors serve CLI's dist/->app/ fallback), fixing postinstall-support.test.ts after cheaper-inference#3124. - obsidian-plugin-e2e: cheaper-inference#3077 committed the e2e test but NEVER committed its dependency obsidian-plugin/src/server.ts (un-ignored but unstaged) nor the 'obsidian' npm pkg, so it crashed with ERR_MODULE_NOT_FOUND on every fresh checkout. Load the runtime values dynamically and skip the suite when absent (unit sync logic stays covered by obsidian-plugin-sync.test.ts).
…heaper-inference#3077) Integrated into release/v3.8.9. Fixes applied: removed .serena/ from repo (gitignored), resolved import conflicts preserving agentSkillTools, fixed TS errors.
….ts (dynamic tools) The Lint job's check:any-budget:t11 (string-blind /\bany\b/ regex) failed on: - open-sse/executors/cursor.ts: the WORD 'any' in cheaper-inference#3104's tool-commit/output- constraint prompt strings — zero real TS `any` in the file. - open-sse/mcp-server/server.ts: 3 `(toolDef: any)` in dynamic memory/skill/ compression tool-registration loops (cheaper-inference#3077), guarded by existing @ts-ignore. Both are v3.8.9-introduced and benign; set the per-file baseline to the count.
…drift + cheaper-inference#3100 dedup) Full CI surfaced real failures that local subsets missed (gh-merged PRs bypass the hooks that run these gates): - typecheck:core (Lint job): 3 now-unused @ts-expect-error in mcp-server/server.ts (cheaper-inference#3077 dynamic tool loops) → @ts-ignore (lenient, no TS2578). - pack-artifact-policy.test.ts: build-reorg (cheaper-inference#3124) renamed app/->dist/; the test still asserted app/ paths + REQUIRED order (it sorts alphabetically). - electron-packaging.test.ts: extraResources from .next/electron-standalone -> .build/electron-standalone (cheaper-inference#3124). - glm-provider-model-import-route.test.ts: two GLM connections shared one apiKey, so cheaper-inference#3100 (cheaper-inference#3023) dedup collapsed them → only one discovery fetch. Distinct keys. Remaining CI flakes (batch expiration, ModelSync self-fetch) pass in isolation — concurrency/port flakiness under --test-concurrency=4, not real failures.
…plugin e2e - hasStandaloneAppBundle now accepts the legacy app/ bundle too (mirrors serve CLI's dist/->app/ fallback), fixing postinstall-support.test.ts after cheaper-inference#3124. - obsidian-plugin-e2e: cheaper-inference#3077 committed the e2e test but NEVER committed its dependency obsidian-plugin/src/server.ts (un-ignored but unstaged) nor the 'obsidian' npm pkg, so it crashed with ERR_MODULE_NOT_FOUND on every fresh checkout. Load the runtime values dynamically and skip the suite when absent (unit sync logic stays covered by obsidian-plugin-sync.test.ts).
Summary
Adds Obsidian as a context source via the Local REST API community plugin, providing 24 MCP tools scoped under
read:obsidianandwrite:obsidian.This follows the same pattern established by the Notion context source PR.
Requirements
127.0.0.1:27125). The API token is configured in OmniRoute Settings → Context Sources.What's included
obsidian-plugin/— community plugin for the sync serverNotes
obsidian-second-brainworkflows