Repository navigation
feat(observability): add Obsidian context source with 24 MCP tools #3077
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
46b2d93
fa27f87
d080d9f
2e3638e
7940d7d
eaa8d5a
e9de9b0
9892e3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,7 @@ import { | |
| } from "./schemas/tools.ts"; | ||
| import { startMcpHeartbeat } from "./runtimeHeartbeat.ts"; | ||
|
|
||
| import { z } from "zod"; | ||
| import { closeAuditDb, logToolCall } from "./audit.ts"; | ||
| import { | ||
| evaluateToolScopes, | ||
|
|
@@ -76,10 +77,13 @@ import { | |
| import { memoryTools } from "./tools/memoryTools.ts"; | ||
| import { skillTools } from "./tools/skillTools.ts"; | ||
| import { agentSkillTools } from "./tools/agentSkillTools.ts"; | ||
| import { skillRegistry } from "../../src/lib/skills/registry.ts"; | ||
| import { skillExecutor } from "../../src/lib/skills/executor.ts"; | ||
| import { pluginTools } from "./tools/pluginTools.ts"; | ||
| import { compressionTools } from "./tools/compressionTools.ts"; | ||
| import { gamificationTools } from "./tools/gamificationTools.ts"; | ||
| import { notionTools } from "./tools/notionTools.ts"; | ||
| import { obsidianTools } from "./tools/obsidianTools.ts"; | ||
| import { compressMcpRegistryMetadata } from "./descriptionCompressor.ts"; | ||
| import { smartFilterText } from "../services/compression/engines/mcpAccessibility/index.ts"; | ||
| import { | ||
|
|
@@ -93,7 +97,6 @@ import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRoute | |
| // ============ Configuration ============ | ||
|
|
||
| const OMNIROUTE_BASE_URL = resolveOmniRouteBaseUrl(); | ||
| const OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY || ""; | ||
| const MCP_ENFORCE_SCOPES = process.env.OMNIROUTE_MCP_ENFORCE_SCOPES === "true"; | ||
| const MCP_ALLOWED_SCOPES = new Set( | ||
| (process.env.OMNIROUTE_MCP_SCOPES || "") | ||
|
|
@@ -108,7 +111,8 @@ const TOTAL_MCP_TOOL_COUNT = | |
| Object.keys(agentSkillTools).length + | ||
| gamificationTools.length + | ||
| pluginTools.length + | ||
| notionTools.length; | ||
| notionTools.length + | ||
| obsidianTools.length; | ||
|
|
||
| type JsonRecord = Record<string, unknown>; | ||
|
|
||
|
|
@@ -186,11 +190,16 @@ function normalizeComboModels( | |
| /** | ||
| * Internal fetch helper that calls OmniRoute API endpoints. | ||
| */ | ||
| function getOmniRouteApiKey(): string { | ||
| return process.env.OMNIROUTE_API_KEY || ""; | ||
| } | ||
|
|
||
| async function omniRouteFetch(path: string, options: RequestInit = {}): Promise<unknown> { | ||
| const url = `${OMNIROUTE_BASE_URL}${path}`; | ||
| const apiKey = getOmniRouteApiKey(); | ||
| const headers: Record<string, string> = { | ||
| "Content-Type": "application/json", | ||
| ...(OMNIROUTE_API_KEY ? { Authorization: `Bearer ${OMNIROUTE_API_KEY}` } : {}), | ||
| ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), | ||
| ...((options.headers as Record<string, string>) || {}), | ||
| }; | ||
|
|
||
|
|
@@ -666,6 +675,17 @@ export function createMcpServer(): McpServer { | |
| return registerResource(name, uriOrTemplate as never, metadata as never, readCallback as never); | ||
| }) as typeof server.registerResource; | ||
|
|
||
| const RESERVED_MCP_NAMES = new Set([ | ||
| ...MCP_TOOLS.map((t) => t.name), | ||
| ...Object.keys(memoryTools), | ||
| ...Object.keys(skillTools), | ||
| ...Object.keys(compressionTools), | ||
| ...pluginTools.map((t) => t.name), | ||
| ...gamificationTools.map((t) => t.name), | ||
| ...obsidianTools.map((t) => t.name), | ||
| ...notionTools.map((t) => t.name), | ||
| ]); | ||
|
|
||
| // Register essential tools | ||
| server.registerTool( | ||
| "omniroute_get_health", | ||
|
|
@@ -965,7 +985,7 @@ export function createMcpServer(): McpServer { | |
| ); | ||
|
|
||
| // ── Memory Tools ────────────────────────────── | ||
| Object.values(memoryTools).forEach((toolDef) => { | ||
| Object.values(memoryTools).forEach((toolDef: any) => { | ||
| server.registerTool( | ||
| toolDef.name, | ||
| { | ||
|
|
@@ -992,7 +1012,7 @@ export function createMcpServer(): McpServer { | |
| }); | ||
|
|
||
| // ── Skill Tools ────────────────────────────── | ||
| Object.values(skillTools).forEach((toolDef) => { | ||
| Object.values(skillTools).forEach((toolDef: any) => { | ||
| server.registerTool( | ||
| toolDef.name, | ||
| { | ||
|
|
@@ -1069,7 +1089,7 @@ export function createMcpServer(): McpServer { | |
| }); | ||
|
|
||
| // ── Compression Tools ───────────────────────── | ||
| Object.values(compressionTools).forEach((toolDef) => { | ||
| Object.values(compressionTools).forEach((toolDef: any) => { | ||
| server.registerTool( | ||
| toolDef.name, | ||
| { | ||
|
|
@@ -1149,6 +1169,106 @@ export function createMcpServer(): McpServer { | |
| ); | ||
| }); | ||
|
|
||
| // ── Obsidian Context Source Tools ───────────── | ||
| obsidianTools.forEach((toolDef) => { | ||
| server.registerTool( | ||
| toolDef.name, | ||
| { | ||
| description: toolDef.description, | ||
| // @ts-ignore: dynamic zod access | ||
| inputSchema: toolDef.inputSchema, | ||
| }, | ||
| withScopeEnforcement( | ||
| toolDef.name, | ||
| async (args) => { | ||
| try { | ||
| const parsedArgs = toolDef.inputSchema.parse(args ?? {}); | ||
| // @ts-ignore: handler expected specific object | ||
| const result = await toolDef.handler(parsedArgs); | ||
| return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; | ||
| } catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; | ||
| } | ||
| }, | ||
| toolDef.scopes | ||
| ) | ||
| ); | ||
| }); | ||
|
|
||
| // ── Notion Context Source Tools ─────────────── | ||
| notionTools.forEach((toolDef) => { | ||
| server.registerTool( | ||
|
Comment on lines
+1199
to
+1201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| toolDef.name, | ||
| { | ||
| description: toolDef.description, | ||
| // @ts-ignore: dynamic zod access | ||
| inputSchema: toolDef.inputSchema, | ||
| }, | ||
| withScopeEnforcement( | ||
| toolDef.name, | ||
| async (args) => { | ||
| try { | ||
| const parsedArgs = toolDef.inputSchema.parse(args ?? {}); | ||
| // @ts-ignore: handler expected specific object | ||
| const result = await toolDef.handler(parsedArgs); | ||
| return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; | ||
| } catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; | ||
| } | ||
| }, | ||
| toolDef.scopes | ||
| ) | ||
| ); | ||
| }); | ||
|
|
||
| // ── Dynamic Skill Tools (from skills table) ── | ||
| const skillToMcpToolName = (skill: { name: string }) => `skill_${skill.name.replace(/[^a-z0-9_-]/gi, "_")}`; | ||
| try { | ||
| const enabledSkills = skillRegistry.list().filter((s) => s.enabled); | ||
| for (const skill of enabledSkills) { | ||
| const toolName = skillToMcpToolName(skill); | ||
| if (RESERVED_MCP_NAMES.has(toolName)) continue; | ||
|
|
||
| server.registerTool( | ||
| toolName, | ||
| { | ||
| description: skill.description, | ||
| inputSchema: z.object({}).passthrough(), | ||
| }, | ||
| withScopeEnforcement( | ||
| toolName, | ||
| async (args, extra) => { | ||
| const scopeContext = resolveCallerScopeContext(extra, Array.from(MCP_ALLOWED_SCOPES)); | ||
| const apiKeyId = scopeContext.callerId || "mcp"; | ||
| try { | ||
| const execution = await skillExecutor.execute( | ||
| skill.name, | ||
| (args ?? {}) as Record<string, unknown>, | ||
| { apiKeyId } | ||
| ); | ||
| return { | ||
| content: [ | ||
| { type: "text" as const, text: JSON.stringify(execution.output, null, 2) }, | ||
| ], | ||
| }; | ||
| } catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| return { | ||
| content: [{ type: "text" as const, text: `Error: ${msg}` }], | ||
| isError: true, | ||
| }; | ||
| } | ||
| }, | ||
| ["execute:skills"] | ||
| ) | ||
| ); | ||
| } | ||
| } catch { | ||
| // Skills not loaded yet — skip dynamic registration until next reconnect | ||
| } | ||
|
|
||
| return server; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
createMcpServer()runs, this secondnotionTools.forEachregisters 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 👍 / 👎.