-
Notifications
You must be signed in to change notification settings - Fork 0
feat(mcp): MCP server, seven memory tools, and stdio integration (T10) #16
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
3b6c625
feat(mcp): MCP server foundation + `dennoh serve` over stdio
goofmint 94688aa
feat(mcp): implement the seven memory tools bridging to core
goofmint 718ee77
test(mcp): per-tool unit tests, real-stdio integration test, setup docs
goofmint db707d9
Update tasks
goofmint fff7aa4
fix: PR #16 のレビューコメントを反映
goofmint 67a17f8
Update config
goofmint File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # Connecting dennoh to Claude Desktop | ||
|
|
||
| `dennoh serve` runs an [MCP](https://modelcontextprotocol.io) server over stdio, | ||
| exposing seven tools (`save_memory`, `update_memory`, `delete_memory`, | ||
| `search_memory`, `list_recent`, `get_note`, `status`). This guide wires it into | ||
| Claude Desktop. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| 1. Install dependencies (from the repo root): | ||
|
|
||
| ```sh | ||
| bun install | ||
| ``` | ||
|
|
||
| 2. Initialize a vault and write the dennoh config. This creates the vault | ||
| directory, a git repo, and `~/Library/Application Support/dennoh/config.json` | ||
| (which `serve` reads to find your vault): | ||
|
|
||
| ```sh | ||
| bun run src/cli/main.ts init | ||
| ``` | ||
|
|
||
| `dennoh serve` does **not** take the vault path as an argument — it always | ||
| reads it from that config file, so `init` must run first. | ||
|
|
||
| ## Configure Claude Desktop | ||
|
|
||
| Claude Desktop reads MCP servers from: | ||
|
|
||
| ```text | ||
| ~/Library/Application Support/Claude/claude_desktop_config.json | ||
| ``` | ||
|
|
||
| Add a `dennoh` entry under `mcpServers`. Use an **absolute path** to this | ||
| repository — Claude Desktop launches the command from its own working | ||
| directory, so relative paths will not resolve. | ||
|
|
||
| ### Option A — run from source (development) | ||
|
|
||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "dennoh": { | ||
| "command": "bun", | ||
| "args": ["run", "/absolute/path/to/dennoh/src/cli/main.ts", "serve"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Option B — run a built bundle | ||
|
|
||
| Build a single-file bundle first: | ||
|
|
||
| ```sh | ||
| bun run build # emits dist/cli.js | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "dennoh": { | ||
| "command": "bun", | ||
| "args": ["/absolute/path/to/dennoh/dist/cli.js", "serve"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Notes: | ||
|
|
||
| - `command` must be resolvable on Claude Desktop's `PATH` (use the absolute path | ||
| to `bun` — e.g. `/Users/you/.bun/bin/bun` — if `bun` is not found). | ||
| - No `env` block is required: `serve` locates the vault through the config file | ||
| under your home directory. To force a language, add | ||
| `"env": { "DENNOH_LANG": "en" }`. | ||
| - stdout carries only the JSON-RPC protocol stream; all logs go to stderr, so | ||
| they will not corrupt the MCP connection. | ||
|
|
||
| Restart Claude Desktop after editing the file so it re-reads the config. | ||
|
|
||
| ## Verify it works | ||
|
|
||
| 1. Open Claude Desktop. The `dennoh` tools should appear in the tools menu | ||
| (the hammer/▶ icon). If they do not, check Claude Desktop's MCP logs at | ||
| `~/Library/Logs/Claude/`. | ||
|
|
||
| 2. Ask Claude to save something, e.g.: | ||
|
|
||
| > Save a memory: "Trying out dennoh #demo @setup" | ||
|
|
||
| Claude calls `save_memory`, which returns the new note's metadata. | ||
|
|
||
| 3. Confirm the markdown file was created on disk. Notes live under | ||
| `<vault>/YYYY/MM/DD/<uuid>.md`: | ||
|
|
||
| ```sh | ||
| # Replace <vault> with your configured vaultPath | ||
| ls "<vault>/$(date +%Y/%m/%d)/" | ||
| cat "<vault>/$(date +%Y/%m/%d)/"*.md | ||
| ``` | ||
|
|
||
| You should see the note with YAML frontmatter (`createdAt`, `updatedAt`, | ||
| `source`, `projects`, `tags`) followed by your text. The `#demo` mention is | ||
| captured under `projects` and `@setup` under `tags`. | ||
|
|
||
| 4. (Optional) Ask Claude to `search_memory` for `demo` or call `status` to see | ||
| the indexed note count. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| - **Tools don't appear / server fails to start** — run the same command in a | ||
| terminal to see the error on stderr: | ||
|
|
||
| ```sh | ||
| echo '' | bun run /absolute/path/to/dennoh/src/cli/main.ts serve | ||
| ``` | ||
|
|
||
| A `dennoh config not found …` message means `init` has not been run. | ||
|
|
||
| - **`bun: command not found` in Claude Desktop** — set `command` to the absolute | ||
| path of the `bun` binary (`which bun`). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import type { Database } from "bun:sqlite"; | ||
|
|
||
| import { type CliIO, readError } from "@/cli/types"; | ||
| import { readConfig } from "@/config"; | ||
| import { closeDatabase, openDatabase, runMigrations } from "@/db"; | ||
| import { log } from "@/log"; | ||
| import { createMcpServer, startStdioServer } from "@/mcp"; | ||
|
|
||
| // `dennoh serve` — run the MCP server over stdio. stdout is reserved for the | ||
| // JSON-RPC protocol stream, so this handler emits diagnostics only via io.stderr | ||
| // and the log module (stderr-only). It blocks until the client disconnects. | ||
| export async function serveCommand(args: string[], io: CliIO): Promise<number> { | ||
| if (args.length > 0) { | ||
| io.stderr(`Unexpected arguments for 'serve': ${args.join(" ")}\n`); | ||
| return 1; | ||
| } | ||
|
|
||
| let vaultPath: string; | ||
| try { | ||
| vaultPath = readConfig().vaultPath; | ||
| } catch (e) { | ||
| io.stderr(`${readError(e)}\n`); | ||
| return 1; | ||
| } | ||
|
|
||
| // Open the index first, with its own error handling: openDatabase creates the | ||
| // .dennoh dir and opens SQLite (and closes itself on internal failure), so a | ||
| // failure here means there is no handle to clean up. Handling it before the | ||
| // try/finally below keeps closeDatabase from ever running on an unopened db. | ||
| let db: Database; | ||
| try { | ||
| db = openDatabase(vaultPath); | ||
| } catch (e) { | ||
| io.stderr(`${readError(e)}\n`); | ||
| return 1; | ||
| } | ||
|
|
||
| // From here the handle is open, so runMigrations and the server run under a | ||
| // try/finally that always closes it — even if migration or startup throws. | ||
| // runMigrations is idempotent (no-ops when the schema is current), so this | ||
| // also covers a brand-new vault where `serve` first touches the database. | ||
| try { | ||
| runMigrations(db); | ||
| const server = createMcpServer({ db, vaultPath }); | ||
| log.info("mcp: serving over stdio", { vaultPath }); | ||
| await startStdioServer(server); | ||
| return 0; | ||
| } catch (e) { | ||
| io.stderr(`${readError(e)}\n`); | ||
| return 1; | ||
| } finally { | ||
| closeDatabase(db); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export {}; | ||
| export { SERVER_NAME, createMcpServer, startStdioServer } from "./server"; | ||
| export type { McpContext, StatusResult } from "./types"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; | ||
|
|
||
| import pkg from "../../package.json" with { type: "json" }; | ||
|
|
||
| import { registerAllTools } from "./tools"; | ||
| import type { McpContext } from "./types"; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // MCP server identity advertised to clients during `initialize`. The name is a | ||
| // stable constant; the version tracks package.json so a single bump propagates. | ||
| export const SERVER_NAME = "dennoh"; | ||
|
|
||
| // Build the MCP server instance and register all dennoh tools against the given | ||
| // context (the open index + vault root). Tool handlers bridge into the core | ||
| // layer; see src/mcp/tools. | ||
| export function createMcpServer(context: McpContext): McpServer { | ||
| const server = new McpServer({ name: SERVER_NAME, version: pkg.version }); | ||
| registerAllTools(server, context); | ||
| return server; | ||
| } | ||
|
|
||
| // Connect `server` to a transport (stdio by default) and resolve only once the | ||
| // connection closes — a client disconnect or stdin EOF. Resolving on close (not | ||
| // on connect) lets `dennoh serve` keep the process alive for the server's | ||
| // lifetime and run cleanup afterwards. The transport is injectable so tests can | ||
| // drive the lifecycle with an in-memory pair instead of real stdio. | ||
| export async function startStdioServer( | ||
| server: McpServer, | ||
| transport: Transport = new StdioServerTransport() | ||
| ): Promise<void> { | ||
| await server.connect(transport); | ||
| await new Promise<void>((resolve) => { | ||
| server.server.onclose = () => resolve(); | ||
| }); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { deleteMemory } from "@/core/memory"; | ||
|
|
||
| import type { McpContext } from "../types"; | ||
| import { toolError, toolOk } from "./result"; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const DESCRIPTION = | ||
| "Delete a note by id: removes the markdown file, soft-deletes the index row, and records a git commit. Returns { id, deleted: true } on success, or an error result if the note does not exist."; | ||
|
|
||
| export function registerDeleteMemory(server: McpServer, context: McpContext): void { | ||
| server.registerTool( | ||
| "delete_memory", | ||
| { | ||
| description: DESCRIPTION, | ||
| inputSchema: { | ||
| id: z.string().min(1), | ||
| }, | ||
| }, | ||
| async ({ id }) => { | ||
| try { | ||
| await deleteMemory(context.db, context.vaultPath, id); | ||
| return toolOk({ id, deleted: true }); | ||
| } catch (e) { | ||
| return toolError(e); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.