granite-mem 0.1.2 → 0.1.4
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.
- package/README.md +44 -4
- package/dist/index.js +1181 -46
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
4
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import fs3 from "fs";
|
|
8
|
+
import path3 from "path";
|
|
9
9
|
|
|
10
10
|
// src/core/config.ts
|
|
11
11
|
import fs from "fs";
|
|
@@ -116,59 +116,76 @@ function loadConfig(vaultRoot) {
|
|
|
116
116
|
return parsed;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// src/core/vault.ts
|
|
120
|
+
import fs2 from "fs";
|
|
121
|
+
import os from "os";
|
|
122
|
+
import path2 from "path";
|
|
123
|
+
var DEFAULT_VAULT_DIRNAME = ".granite";
|
|
124
|
+
function hasVaultConfig(dir) {
|
|
125
|
+
return fs2.existsSync(path2.join(dir, CONFIG_FILENAME));
|
|
126
|
+
}
|
|
127
|
+
function getDefaultVaultRoot(homeDir = os.homedir()) {
|
|
128
|
+
return path2.resolve(homeDir, DEFAULT_VAULT_DIRNAME);
|
|
129
|
+
}
|
|
130
|
+
function findVaultRoot(from) {
|
|
131
|
+
let dir = from ? path2.resolve(from) : process.cwd();
|
|
132
|
+
while (true) {
|
|
133
|
+
if (hasVaultConfig(dir)) {
|
|
134
|
+
return dir;
|
|
135
|
+
}
|
|
136
|
+
const parent = path2.dirname(dir);
|
|
137
|
+
if (parent === dir) break;
|
|
138
|
+
dir = parent;
|
|
139
|
+
}
|
|
140
|
+
const defaultVaultRoot = getDefaultVaultRoot();
|
|
141
|
+
return hasVaultConfig(defaultVaultRoot) ? defaultVaultRoot : null;
|
|
142
|
+
}
|
|
143
|
+
function requireVaultRoot(from) {
|
|
144
|
+
const root = findVaultRoot(from);
|
|
145
|
+
if (!root) {
|
|
146
|
+
throw new Error(`No Granite vault found. Run "mem init" to create ${getDefaultVaultRoot()}.`);
|
|
147
|
+
}
|
|
148
|
+
return root;
|
|
149
|
+
}
|
|
150
|
+
function getIndexDbPath(vaultRoot) {
|
|
151
|
+
if (path2.basename(path2.resolve(vaultRoot)) === DEFAULT_VAULT_DIRNAME) {
|
|
152
|
+
return path2.join(vaultRoot, "index.db");
|
|
153
|
+
}
|
|
154
|
+
return path2.join(vaultRoot, ".granite", "index.db");
|
|
155
|
+
}
|
|
156
|
+
|
|
119
157
|
// src/commands/init.ts
|
|
120
|
-
function initVault(dir) {
|
|
121
|
-
const targetDir =
|
|
122
|
-
|
|
158
|
+
function initVault(dir = getDefaultVaultRoot()) {
|
|
159
|
+
const targetDir = path3.resolve(dir);
|
|
160
|
+
fs3.mkdirSync(targetDir, { recursive: true });
|
|
161
|
+
if (fs3.existsSync(path3.join(targetDir, CONFIG_FILENAME))) {
|
|
123
162
|
console.error(`Vault already exists in ${targetDir}`);
|
|
124
163
|
process.exit(1);
|
|
125
164
|
}
|
|
126
165
|
writeDefaultConfig(targetDir);
|
|
127
|
-
|
|
166
|
+
fs3.mkdirSync(path3.dirname(getIndexDbPath(targetDir)), { recursive: true });
|
|
128
167
|
const config = loadConfig(targetDir);
|
|
129
168
|
for (const typeConfig of Object.values(config.note_types)) {
|
|
130
|
-
|
|
169
|
+
fs3.mkdirSync(path3.join(targetDir, typeConfig.folder), { recursive: true });
|
|
131
170
|
}
|
|
132
171
|
console.log(`Vault initialized in ${targetDir}`);
|
|
133
172
|
console.log("");
|
|
134
173
|
console.log("Created:");
|
|
135
174
|
console.log(` ${CONFIG_FILENAME}`);
|
|
136
|
-
|
|
175
|
+
const indexDir = path3.relative(targetDir, path3.dirname(getIndexDbPath(targetDir)));
|
|
176
|
+
if (indexDir) {
|
|
177
|
+
console.log(` ${indexDir}/`);
|
|
178
|
+
}
|
|
137
179
|
for (const [name, typeConfig] of Object.entries(config.note_types)) {
|
|
138
180
|
console.log(` ${typeConfig.folder}/ (${name})`);
|
|
139
181
|
}
|
|
140
182
|
console.log("");
|
|
141
|
-
console.log('Next: mem new
|
|
183
|
+
console.log('Next: mem new "My first note" --type fleeting');
|
|
142
184
|
}
|
|
143
185
|
|
|
144
186
|
// src/commands/new.ts
|
|
145
187
|
import fs6 from "fs";
|
|
146
188
|
|
|
147
|
-
// src/core/vault.ts
|
|
148
|
-
import fs3 from "fs";
|
|
149
|
-
import path3 from "path";
|
|
150
|
-
function findVaultRoot(from) {
|
|
151
|
-
let dir = from ? path3.resolve(from) : process.cwd();
|
|
152
|
-
while (true) {
|
|
153
|
-
if (fs3.existsSync(path3.join(dir, CONFIG_FILENAME))) {
|
|
154
|
-
return dir;
|
|
155
|
-
}
|
|
156
|
-
const parent = path3.dirname(dir);
|
|
157
|
-
if (parent === dir) return null;
|
|
158
|
-
dir = parent;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
function requireVaultRoot(from) {
|
|
162
|
-
const root = findVaultRoot(from);
|
|
163
|
-
if (!root) {
|
|
164
|
-
throw new Error('Not inside a Granite vault. Run "mem init" to create one.');
|
|
165
|
-
}
|
|
166
|
-
return root;
|
|
167
|
-
}
|
|
168
|
-
function getIndexDbPath(vaultRoot) {
|
|
169
|
-
return path3.join(vaultRoot, ".granite", "index.db");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
189
|
// src/core/note.ts
|
|
173
190
|
import fs4 from "fs";
|
|
174
191
|
import path4 from "path";
|
|
@@ -516,8 +533,8 @@ function countNoteFiles(vaultRoot, config) {
|
|
|
516
533
|
}
|
|
517
534
|
function upsertNoteAndLinks(db, note) {
|
|
518
535
|
const upsertNote = db.prepare(`
|
|
519
|
-
INSERT INTO notes (slug, id, title, type, created, modified, tags, aliases, body, filepath)
|
|
520
|
-
VALUES (@slug, @id, @title, @type, @created, @modified, @tags, @aliases, @body, @filepath)
|
|
536
|
+
INSERT INTO notes (slug, id, title, type, created, modified, tags, aliases, body, filepath, status, source)
|
|
537
|
+
VALUES (@slug, @id, @title, @type, @created, @modified, @tags, @aliases, @body, @filepath, @status, @source)
|
|
521
538
|
ON CONFLICT(slug) DO UPDATE SET
|
|
522
539
|
id = excluded.id,
|
|
523
540
|
title = excluded.title,
|
|
@@ -527,7 +544,9 @@ function upsertNoteAndLinks(db, note) {
|
|
|
527
544
|
tags = excluded.tags,
|
|
528
545
|
aliases = excluded.aliases,
|
|
529
546
|
body = excluded.body,
|
|
530
|
-
filepath = excluded.filepath
|
|
547
|
+
filepath = excluded.filepath,
|
|
548
|
+
status = excluded.status,
|
|
549
|
+
source = excluded.source
|
|
531
550
|
`);
|
|
532
551
|
const deleteLinks = db.prepare("DELETE FROM links WHERE source_slug = ?");
|
|
533
552
|
const insertLink = db.prepare(`
|
|
@@ -545,7 +564,9 @@ function upsertNoteAndLinks(db, note) {
|
|
|
545
564
|
tags: JSON.stringify(note.frontmatter.tags),
|
|
546
565
|
aliases: JSON.stringify(note.frontmatter.aliases),
|
|
547
566
|
body: note.body,
|
|
548
|
-
filepath: note.filepath
|
|
567
|
+
filepath: note.filepath,
|
|
568
|
+
status: note.frontmatter.status ?? "active",
|
|
569
|
+
source: note.frontmatter.source ?? "human"
|
|
549
570
|
});
|
|
550
571
|
deleteLinks.run(note.slug);
|
|
551
572
|
const links = parseWikilinks(note.body);
|
|
@@ -1337,7 +1358,8 @@ function showCommand(slug, options) {
|
|
|
1337
1358
|
}
|
|
1338
1359
|
|
|
1339
1360
|
// src/core/search.ts
|
|
1340
|
-
function searchNotes(db, query) {
|
|
1361
|
+
function searchNotes(db, query, limit = 20) {
|
|
1362
|
+
const cappedLimit = Math.max(1, Math.min(limit, 100));
|
|
1341
1363
|
const stmt = db.prepare(`
|
|
1342
1364
|
SELECT
|
|
1343
1365
|
n.slug,
|
|
@@ -1348,9 +1370,9 @@ function searchNotes(db, query) {
|
|
|
1348
1370
|
JOIN notes n ON n.rowid = notes_fts.rowid
|
|
1349
1371
|
WHERE notes_fts MATCH ?
|
|
1350
1372
|
ORDER BY rank
|
|
1351
|
-
LIMIT
|
|
1373
|
+
LIMIT ?
|
|
1352
1374
|
`);
|
|
1353
|
-
const rows = stmt.all(query);
|
|
1375
|
+
const rows = stmt.all(query, cappedLimit);
|
|
1354
1376
|
return rows.map((r) => ({
|
|
1355
1377
|
slug: r.slug,
|
|
1356
1378
|
title: r.title,
|
|
@@ -1942,11 +1964,1111 @@ function printRecommendations(vaultRoot, config, filepath, strategy) {
|
|
|
1942
1964
|
}
|
|
1943
1965
|
}
|
|
1944
1966
|
|
|
1967
|
+
// src/commands/mcp.ts
|
|
1968
|
+
import path9 from "path";
|
|
1969
|
+
|
|
1970
|
+
// src/mcp/server.ts
|
|
1971
|
+
import { serve as serve2 } from "@hono/node-server";
|
|
1972
|
+
import { Hono as Hono2 } from "hono";
|
|
1973
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1974
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1975
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
1976
|
+
import * as z from "zod/v4";
|
|
1977
|
+
|
|
1978
|
+
// src/version.ts
|
|
1979
|
+
var GRANITE_VERSION = "0.1.2";
|
|
1980
|
+
|
|
1981
|
+
// src/mcp/server.ts
|
|
1982
|
+
var readOnlyAnnotations = {
|
|
1983
|
+
readOnlyHint: true,
|
|
1984
|
+
destructiveHint: false,
|
|
1985
|
+
idempotentHint: true,
|
|
1986
|
+
openWorldHint: false
|
|
1987
|
+
};
|
|
1988
|
+
var writeAnnotations = {
|
|
1989
|
+
readOnlyHint: false,
|
|
1990
|
+
destructiveHint: false,
|
|
1991
|
+
idempotentHint: false,
|
|
1992
|
+
openWorldHint: false
|
|
1993
|
+
};
|
|
1994
|
+
var fieldDefinitionSchema = z.object({
|
|
1995
|
+
type: z.enum(["text", "date", "number", "boolean", "wikilink", "list", "enum"]),
|
|
1996
|
+
of: z.string().optional(),
|
|
1997
|
+
options: z.array(z.string()).optional(),
|
|
1998
|
+
required: z.boolean().optional(),
|
|
1999
|
+
default: z.string().optional(),
|
|
2000
|
+
description: z.string().optional()
|
|
2001
|
+
});
|
|
2002
|
+
var noteTypeInfoSchema = z.object({
|
|
2003
|
+
name: z.string(),
|
|
2004
|
+
description: z.string(),
|
|
2005
|
+
folder: z.string(),
|
|
2006
|
+
line_limit: z.number(),
|
|
2007
|
+
warn_only: z.boolean(),
|
|
2008
|
+
slug_format: z.enum(["title", "date"]),
|
|
2009
|
+
instructions: z.string().optional(),
|
|
2010
|
+
fields: z.record(z.string(), fieldDefinitionSchema).optional()
|
|
2011
|
+
});
|
|
2012
|
+
var noteSummarySchema = z.object({
|
|
2013
|
+
slug: z.string(),
|
|
2014
|
+
title: z.string(),
|
|
2015
|
+
type: z.string(),
|
|
2016
|
+
created: z.string(),
|
|
2017
|
+
modified: z.string(),
|
|
2018
|
+
tags: z.array(z.string()),
|
|
2019
|
+
aliases: z.array(z.string()),
|
|
2020
|
+
status: z.enum(["inbox", "active", "archived"]),
|
|
2021
|
+
source: z.enum(["human", "agent", "extraction"]),
|
|
2022
|
+
filepath: z.string(),
|
|
2023
|
+
resource_uri: z.string()
|
|
2024
|
+
});
|
|
2025
|
+
var wikiLinkSchema = z.object({
|
|
2026
|
+
raw: z.string(),
|
|
2027
|
+
target: z.string(),
|
|
2028
|
+
display: z.string(),
|
|
2029
|
+
resolved: z.boolean(),
|
|
2030
|
+
resolved_slug: z.string().optional()
|
|
2031
|
+
});
|
|
2032
|
+
var noteDetailsSchema = noteSummarySchema.extend({
|
|
2033
|
+
body: z.string(),
|
|
2034
|
+
frontmatter: z.record(z.string(), z.unknown()),
|
|
2035
|
+
outgoing_links: z.array(wikiLinkSchema)
|
|
2036
|
+
});
|
|
2037
|
+
var searchResultSchema = z.object({
|
|
2038
|
+
slug: z.string(),
|
|
2039
|
+
title: z.string(),
|
|
2040
|
+
snippet: z.string(),
|
|
2041
|
+
score: z.number()
|
|
2042
|
+
});
|
|
2043
|
+
var backlinkSchema = z.object({
|
|
2044
|
+
source_slug: z.string(),
|
|
2045
|
+
source_title: z.string(),
|
|
2046
|
+
context: z.string()
|
|
2047
|
+
});
|
|
2048
|
+
var linkSuggestionSchema = z.object({
|
|
2049
|
+
target_slug: z.string(),
|
|
2050
|
+
target_title: z.string(),
|
|
2051
|
+
mentions: z.number()
|
|
2052
|
+
});
|
|
2053
|
+
var recommendationSchema = z.object({
|
|
2054
|
+
additions: z.array(z.object({ text: z.string() })),
|
|
2055
|
+
links: z.array(z.object({
|
|
2056
|
+
slug: z.string(),
|
|
2057
|
+
title: z.string(),
|
|
2058
|
+
type: z.string(),
|
|
2059
|
+
reason: z.string(),
|
|
2060
|
+
source: z.enum(["mention", "search"])
|
|
2061
|
+
})),
|
|
2062
|
+
tags: z.array(z.object({
|
|
2063
|
+
tag: z.string(),
|
|
2064
|
+
weight: z.number(),
|
|
2065
|
+
source_slugs: z.array(z.string())
|
|
2066
|
+
})),
|
|
2067
|
+
next_steps: z.array(z.object({
|
|
2068
|
+
type: z.string(),
|
|
2069
|
+
title_hint: z.string().optional(),
|
|
2070
|
+
reason: z.string()
|
|
2071
|
+
}))
|
|
2072
|
+
});
|
|
2073
|
+
var doctorIssueSchema = z.object({
|
|
2074
|
+
level: z.enum(["error", "warning", "info"]),
|
|
2075
|
+
file: z.string(),
|
|
2076
|
+
message: z.string()
|
|
2077
|
+
});
|
|
2078
|
+
var vaultOverviewSchema = z.object({
|
|
2079
|
+
vault_root: z.string(),
|
|
2080
|
+
vault_name: z.string(),
|
|
2081
|
+
default_type: z.string(),
|
|
2082
|
+
auto_rebuild: z.boolean(),
|
|
2083
|
+
index_last_rebuild: z.string().optional(),
|
|
2084
|
+
note_count: z.number(),
|
|
2085
|
+
notes_by_type: z.record(z.string(), z.number()),
|
|
2086
|
+
recent_notes: z.array(noteSummarySchema)
|
|
2087
|
+
});
|
|
2088
|
+
function createGraniteMcpServer(runtime) {
|
|
2089
|
+
const server = new McpServer(
|
|
2090
|
+
{
|
|
2091
|
+
name: "granite",
|
|
2092
|
+
version: GRANITE_VERSION,
|
|
2093
|
+
title: "Granite MCP Server"
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
capabilities: { logging: {} },
|
|
2097
|
+
instructions: [
|
|
2098
|
+
"Granite exposes a local-first markdown vault.",
|
|
2099
|
+
"Prefer read tools and resources first, then write with granite_create_note or granite_update_note.",
|
|
2100
|
+
"Resources are read-only views; notes live on disk and the SQLite index is derived state.",
|
|
2101
|
+
"Use granite_recommend_note_actions to preserve Granite\u2019s capture -> link -> recommend loop."
|
|
2102
|
+
].join(" ")
|
|
2103
|
+
}
|
|
2104
|
+
);
|
|
2105
|
+
registerTools(server, runtime);
|
|
2106
|
+
registerResources(server, runtime);
|
|
2107
|
+
registerPrompts(server, runtime);
|
|
2108
|
+
return server;
|
|
2109
|
+
}
|
|
2110
|
+
async function startGraniteMcpStdioServer(runtime) {
|
|
2111
|
+
const server = createGraniteMcpServer(runtime);
|
|
2112
|
+
const transport = new StdioServerTransport();
|
|
2113
|
+
await server.connect(transport);
|
|
2114
|
+
console.error(`Granite MCP server listening on stdio for ${runtime.vaultRoot}`);
|
|
2115
|
+
}
|
|
2116
|
+
function startGraniteMcpHttpServer(runtime, options) {
|
|
2117
|
+
const app = createGraniteMcpHttpApp(runtime, options);
|
|
2118
|
+
console.error(`Granite MCP server listening on http://${options.host}:${options.port}/mcp`);
|
|
2119
|
+
console.error(`Health check: http://${options.host}:${options.port}/health`);
|
|
2120
|
+
console.error(`Vault: ${runtime.vaultRoot}`);
|
|
2121
|
+
serve2({
|
|
2122
|
+
fetch: app.fetch,
|
|
2123
|
+
hostname: options.host,
|
|
2124
|
+
port: options.port
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
async function withResponseCleanup(response, cleanup) {
|
|
2128
|
+
let cleanedUp = false;
|
|
2129
|
+
const cleanupOnce = async () => {
|
|
2130
|
+
if (cleanedUp) {
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
cleanedUp = true;
|
|
2134
|
+
await cleanup();
|
|
2135
|
+
};
|
|
2136
|
+
if (!response.body) {
|
|
2137
|
+
await cleanupOnce();
|
|
2138
|
+
return response;
|
|
2139
|
+
}
|
|
2140
|
+
const reader = response.body.getReader();
|
|
2141
|
+
const wrappedBody = new ReadableStream({
|
|
2142
|
+
async pull(controller) {
|
|
2143
|
+
try {
|
|
2144
|
+
const { done, value } = await reader.read();
|
|
2145
|
+
if (done) {
|
|
2146
|
+
controller.close();
|
|
2147
|
+
await cleanupOnce();
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
controller.enqueue(value);
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
controller.error(error);
|
|
2153
|
+
await cleanupOnce();
|
|
2154
|
+
}
|
|
2155
|
+
},
|
|
2156
|
+
async cancel(reason) {
|
|
2157
|
+
try {
|
|
2158
|
+
await reader.cancel(reason);
|
|
2159
|
+
} finally {
|
|
2160
|
+
await cleanupOnce();
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
return new Response(wrappedBody, {
|
|
2165
|
+
status: response.status,
|
|
2166
|
+
statusText: response.statusText,
|
|
2167
|
+
headers: response.headers
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
function registerTools(server, runtime) {
|
|
2171
|
+
server.registerTool("granite_get_vault_overview", {
|
|
2172
|
+
title: "Granite Vault Overview",
|
|
2173
|
+
description: "Summarize the current Granite vault, including counts by type and recent notes.",
|
|
2174
|
+
inputSchema: {
|
|
2175
|
+
recent_limit: z.number().int().min(1).max(20).optional().describe("How many recent notes to include. Defaults to 10.")
|
|
2176
|
+
},
|
|
2177
|
+
outputSchema: vaultOverviewSchema,
|
|
2178
|
+
annotations: readOnlyAnnotations
|
|
2179
|
+
}, async ({ recent_limit }) => {
|
|
2180
|
+
const overview = runtime.getVaultOverview(recent_limit ?? 10);
|
|
2181
|
+
return toolResult(
|
|
2182
|
+
overview,
|
|
2183
|
+
`Vault "${overview.vault_name}" with ${overview.note_count} notes.`
|
|
2184
|
+
);
|
|
2185
|
+
});
|
|
2186
|
+
server.registerTool("granite_list_note_types", {
|
|
2187
|
+
title: "Granite Note Types",
|
|
2188
|
+
description: "List the note types configured in the vault.",
|
|
2189
|
+
outputSchema: z.object({
|
|
2190
|
+
default_type: z.string(),
|
|
2191
|
+
note_types: z.array(noteTypeInfoSchema)
|
|
2192
|
+
}),
|
|
2193
|
+
annotations: readOnlyAnnotations
|
|
2194
|
+
}, async () => {
|
|
2195
|
+
const noteTypes = runtime.listNoteTypes();
|
|
2196
|
+
return toolResult({
|
|
2197
|
+
default_type: runtime.getDefaultNoteType(),
|
|
2198
|
+
note_types: noteTypes
|
|
2199
|
+
}, `Loaded ${noteTypes.length} Granite note types.`);
|
|
2200
|
+
});
|
|
2201
|
+
server.registerTool("granite_list_notes", {
|
|
2202
|
+
title: "List Granite Notes",
|
|
2203
|
+
description: "List notes from the Granite vault with optional filters.",
|
|
2204
|
+
inputSchema: {
|
|
2205
|
+
type: z.string().optional().describe("Filter by note type."),
|
|
2206
|
+
status: z.enum(["inbox", "active", "archived"]).optional().describe("Filter by note status."),
|
|
2207
|
+
source: z.enum(["human", "agent", "extraction"]).optional().describe("Filter by note source."),
|
|
2208
|
+
since: z.string().optional().describe("Only notes modified on or after this ISO or YYYY-MM-DD value."),
|
|
2209
|
+
limit: z.number().int().min(1).max(200).optional().describe("Maximum number of notes to return. Defaults to 25.")
|
|
2210
|
+
},
|
|
2211
|
+
outputSchema: z.object({
|
|
2212
|
+
notes: z.array(noteSummarySchema),
|
|
2213
|
+
total: z.number()
|
|
2214
|
+
}),
|
|
2215
|
+
annotations: readOnlyAnnotations
|
|
2216
|
+
}, async (args) => {
|
|
2217
|
+
const notes = runtime.listNotes(args);
|
|
2218
|
+
return toolResult({
|
|
2219
|
+
notes,
|
|
2220
|
+
total: notes.length
|
|
2221
|
+
}, `Returned ${notes.length} note(s).`);
|
|
2222
|
+
});
|
|
2223
|
+
server.registerTool("granite_get_note", {
|
|
2224
|
+
title: "Get Granite Note",
|
|
2225
|
+
description: "Read a Granite note by slug, including frontmatter, body, and resolved outgoing links.",
|
|
2226
|
+
inputSchema: {
|
|
2227
|
+
slug: z.string().describe("The note slug.")
|
|
2228
|
+
},
|
|
2229
|
+
outputSchema: noteDetailsSchema,
|
|
2230
|
+
annotations: readOnlyAnnotations
|
|
2231
|
+
}, async ({ slug }) => {
|
|
2232
|
+
const note = runtime.getNote(slug);
|
|
2233
|
+
return {
|
|
2234
|
+
...toolResult(note, `Loaded "${note.title}" (${note.slug}).`),
|
|
2235
|
+
content: [
|
|
2236
|
+
{ type: "text", text: `Loaded "${note.title}" (${note.slug}).` },
|
|
2237
|
+
createNoteResourceLink(note.title, note.resource_uri)
|
|
2238
|
+
]
|
|
2239
|
+
};
|
|
2240
|
+
});
|
|
2241
|
+
server.registerTool("granite_search_notes", {
|
|
2242
|
+
title: "Search Granite Notes",
|
|
2243
|
+
description: "Run full-text search against the Granite index.",
|
|
2244
|
+
inputSchema: {
|
|
2245
|
+
query: z.string().describe("Full-text query for the SQLite FTS index."),
|
|
2246
|
+
limit: z.number().int().min(1).max(50).optional().describe("Maximum number of search results. Defaults to 10.")
|
|
2247
|
+
},
|
|
2248
|
+
outputSchema: z.object({
|
|
2249
|
+
query: z.string(),
|
|
2250
|
+
results: z.array(searchResultSchema)
|
|
2251
|
+
}),
|
|
2252
|
+
annotations: readOnlyAnnotations
|
|
2253
|
+
}, async ({ query, limit }) => {
|
|
2254
|
+
const results = runtime.search(query, limit ?? 10);
|
|
2255
|
+
return toolResult({
|
|
2256
|
+
query,
|
|
2257
|
+
results
|
|
2258
|
+
}, `Found ${results.length} result(s) for "${query}".`);
|
|
2259
|
+
});
|
|
2260
|
+
server.registerTool("granite_create_note", {
|
|
2261
|
+
title: "Create Granite Note",
|
|
2262
|
+
description: "Create a new note in the Granite vault.",
|
|
2263
|
+
inputSchema: {
|
|
2264
|
+
title: z.string().describe("Note title."),
|
|
2265
|
+
type: z.string().optional().describe("Note type. Defaults to the vault default type."),
|
|
2266
|
+
body: z.string().optional().describe("Optional note body. If omitted, Granite uses the type template."),
|
|
2267
|
+
tags: z.array(z.string()).optional().describe("Tags to add immediately."),
|
|
2268
|
+
aliases: z.array(z.string()).optional().describe("Aliases to add immediately."),
|
|
2269
|
+
status: z.enum(["inbox", "active", "archived"]).optional().describe("Initial note status."),
|
|
2270
|
+
source: z.enum(["human", "agent", "extraction"]).optional().describe("Initial note source.")
|
|
2271
|
+
},
|
|
2272
|
+
outputSchema: z.object({
|
|
2273
|
+
note: noteDetailsSchema,
|
|
2274
|
+
recommendations: recommendationSchema
|
|
2275
|
+
}),
|
|
2276
|
+
annotations: writeAnnotations
|
|
2277
|
+
}, async (args) => {
|
|
2278
|
+
const result = runtime.createNote(args);
|
|
2279
|
+
return toolResult(result, `Created "${result.note.title}" (${result.note.slug}).`);
|
|
2280
|
+
});
|
|
2281
|
+
server.registerTool("granite_capture_note", {
|
|
2282
|
+
title: "Capture Granite Note",
|
|
2283
|
+
description: "Quick-capture a note from free-form text, similar to mem add.",
|
|
2284
|
+
inputSchema: {
|
|
2285
|
+
text: z.string().describe("Raw capture text."),
|
|
2286
|
+
type: z.string().optional().describe("Optional note type override. Defaults to the vault default type."),
|
|
2287
|
+
tags: z.array(z.string()).optional().describe("Tags to add immediately."),
|
|
2288
|
+
aliases: z.array(z.string()).optional().describe("Aliases to add immediately."),
|
|
2289
|
+
status: z.enum(["inbox", "active", "archived"]).optional().describe("Initial note status."),
|
|
2290
|
+
source: z.enum(["human", "agent", "extraction"]).optional().describe("Initial note source.")
|
|
2291
|
+
},
|
|
2292
|
+
outputSchema: z.object({
|
|
2293
|
+
note: noteDetailsSchema,
|
|
2294
|
+
recommendations: recommendationSchema
|
|
2295
|
+
}),
|
|
2296
|
+
annotations: writeAnnotations
|
|
2297
|
+
}, async (args) => {
|
|
2298
|
+
const result = runtime.captureNote(args);
|
|
2299
|
+
return toolResult(result, `Captured "${result.note.title}" (${result.note.slug}).`);
|
|
2300
|
+
});
|
|
2301
|
+
server.registerTool("granite_update_note", {
|
|
2302
|
+
title: "Update Granite Note",
|
|
2303
|
+
description: "Update frontmatter or body fields for an existing Granite note.",
|
|
2304
|
+
inputSchema: {
|
|
2305
|
+
slug: z.string().describe("Slug of the note to update."),
|
|
2306
|
+
title: z.string().optional().describe("Replace the note title."),
|
|
2307
|
+
body: z.string().optional().describe("Replace the entire note body."),
|
|
2308
|
+
append: z.string().optional().describe("Append text to the existing note body."),
|
|
2309
|
+
tags: z.array(z.string()).optional().describe("Tags to add."),
|
|
2310
|
+
aliases: z.array(z.string()).optional().describe("Aliases to add."),
|
|
2311
|
+
status: z.enum(["inbox", "active", "archived"]).optional().describe("New note status."),
|
|
2312
|
+
source: z.enum(["human", "agent", "extraction"]).optional().describe("New note source.")
|
|
2313
|
+
},
|
|
2314
|
+
outputSchema: z.object({
|
|
2315
|
+
note: noteDetailsSchema,
|
|
2316
|
+
recommendations: recommendationSchema
|
|
2317
|
+
}),
|
|
2318
|
+
annotations: writeAnnotations
|
|
2319
|
+
}, async ({ slug, ...updates }) => {
|
|
2320
|
+
const result = runtime.updateNote(slug, updates);
|
|
2321
|
+
return toolResult(result, `Updated "${result.note.title}" (${result.note.slug}).`);
|
|
2322
|
+
});
|
|
2323
|
+
server.registerTool("granite_get_backlinks", {
|
|
2324
|
+
title: "Granite Backlinks",
|
|
2325
|
+
description: "List notes that link to a given Granite note.",
|
|
2326
|
+
inputSchema: {
|
|
2327
|
+
slug: z.string().describe("Slug of the target note.")
|
|
2328
|
+
},
|
|
2329
|
+
outputSchema: z.object({
|
|
2330
|
+
slug: z.string(),
|
|
2331
|
+
backlinks: z.array(backlinkSchema)
|
|
2332
|
+
}),
|
|
2333
|
+
annotations: readOnlyAnnotations
|
|
2334
|
+
}, async ({ slug }) => {
|
|
2335
|
+
const backlinks = runtime.getBacklinks(slug);
|
|
2336
|
+
return toolResult({
|
|
2337
|
+
slug,
|
|
2338
|
+
backlinks
|
|
2339
|
+
}, `Found ${backlinks.length} backlink(s) for "${slug}".`);
|
|
2340
|
+
});
|
|
2341
|
+
server.registerTool("granite_suggest_links", {
|
|
2342
|
+
title: "Suggest Granite Links",
|
|
2343
|
+
description: "Suggest missing wikilinks for a Granite note based on mentions.",
|
|
2344
|
+
inputSchema: {
|
|
2345
|
+
slug: z.string().describe("Slug of the note to inspect.")
|
|
2346
|
+
},
|
|
2347
|
+
outputSchema: z.object({
|
|
2348
|
+
slug: z.string(),
|
|
2349
|
+
suggestions: z.array(linkSuggestionSchema)
|
|
2350
|
+
}),
|
|
2351
|
+
annotations: readOnlyAnnotations
|
|
2352
|
+
}, async ({ slug }) => {
|
|
2353
|
+
const suggestions = runtime.suggestLinks(slug);
|
|
2354
|
+
return toolResult({
|
|
2355
|
+
slug,
|
|
2356
|
+
suggestions
|
|
2357
|
+
}, `Found ${suggestions.length} suggested link(s) for "${slug}".`);
|
|
2358
|
+
});
|
|
2359
|
+
server.registerTool("granite_recommend_note_actions", {
|
|
2360
|
+
title: "Recommend Granite Actions",
|
|
2361
|
+
description: "Recommend follow-up links, tags, additions, and next notes for a Granite note.",
|
|
2362
|
+
inputSchema: {
|
|
2363
|
+
slug: z.string().describe("Slug of the note to analyze.")
|
|
2364
|
+
},
|
|
2365
|
+
outputSchema: z.object({
|
|
2366
|
+
slug: z.string(),
|
|
2367
|
+
recommendations: recommendationSchema
|
|
2368
|
+
}),
|
|
2369
|
+
annotations: readOnlyAnnotations
|
|
2370
|
+
}, async ({ slug }) => {
|
|
2371
|
+
const recommendations = runtime.recommend(slug);
|
|
2372
|
+
return toolResult({
|
|
2373
|
+
slug,
|
|
2374
|
+
recommendations
|
|
2375
|
+
}, recommendationSummary(recommendations));
|
|
2376
|
+
});
|
|
2377
|
+
server.registerTool("granite_run_doctor", {
|
|
2378
|
+
title: "Run Granite Doctor",
|
|
2379
|
+
description: "Validate vault health and report structural issues.",
|
|
2380
|
+
outputSchema: z.object({
|
|
2381
|
+
issues: z.array(doctorIssueSchema),
|
|
2382
|
+
counts: z.object({
|
|
2383
|
+
errors: z.number(),
|
|
2384
|
+
warnings: z.number(),
|
|
2385
|
+
info: z.number()
|
|
2386
|
+
})
|
|
2387
|
+
}),
|
|
2388
|
+
annotations: readOnlyAnnotations
|
|
2389
|
+
}, async () => {
|
|
2390
|
+
const result = runtime.runDoctor();
|
|
2391
|
+
return toolResult(
|
|
2392
|
+
result,
|
|
2393
|
+
`${result.counts.errors} error(s), ${result.counts.warnings} warning(s), ${result.counts.info} info item(s).`
|
|
2394
|
+
);
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
function registerResources(server, runtime) {
|
|
2398
|
+
server.registerResource("granite-vault-config", "granite://vault/config", {
|
|
2399
|
+
title: "Granite Config",
|
|
2400
|
+
description: "Raw granite.yml configuration for the current vault.",
|
|
2401
|
+
mimeType: "text/yaml"
|
|
2402
|
+
}, async () => ({
|
|
2403
|
+
contents: [{
|
|
2404
|
+
uri: "granite://vault/config",
|
|
2405
|
+
text: runtime.readVaultConfigRaw(),
|
|
2406
|
+
mimeType: "text/yaml"
|
|
2407
|
+
}]
|
|
2408
|
+
}));
|
|
2409
|
+
server.registerResource("granite-vault-overview", "granite://vault/overview", {
|
|
2410
|
+
title: "Granite Vault Overview",
|
|
2411
|
+
description: "Structured summary of the current vault.",
|
|
2412
|
+
mimeType: "application/json"
|
|
2413
|
+
}, async () => ({
|
|
2414
|
+
contents: [{
|
|
2415
|
+
uri: "granite://vault/overview",
|
|
2416
|
+
text: runtime.readVaultOverviewJson(),
|
|
2417
|
+
mimeType: "application/json"
|
|
2418
|
+
}]
|
|
2419
|
+
}));
|
|
2420
|
+
server.registerResource("granite-note-types", "granite://vault/types", {
|
|
2421
|
+
title: "Granite Note Types",
|
|
2422
|
+
description: "Structured list of the note types configured in the current vault.",
|
|
2423
|
+
mimeType: "application/json"
|
|
2424
|
+
}, async () => ({
|
|
2425
|
+
contents: [{
|
|
2426
|
+
uri: "granite://vault/types",
|
|
2427
|
+
text: runtime.readVaultTypesJson(),
|
|
2428
|
+
mimeType: "application/json"
|
|
2429
|
+
}]
|
|
2430
|
+
}));
|
|
2431
|
+
const noteTemplate = new ResourceTemplate("granite://notes/{slug}", {
|
|
2432
|
+
list: void 0,
|
|
2433
|
+
complete: {
|
|
2434
|
+
slug: async (value) => runtime.completeSlugs(value)
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
server.registerResource("granite-note", noteTemplate, {
|
|
2438
|
+
title: "Granite Note",
|
|
2439
|
+
description: "Read a Granite note as markdown with YAML frontmatter.",
|
|
2440
|
+
mimeType: "text/markdown"
|
|
2441
|
+
}, async (uri, variables) => {
|
|
2442
|
+
const variable = variables.slug;
|
|
2443
|
+
const slug = Array.isArray(variable) ? variable[0] : variable;
|
|
2444
|
+
if (!slug) {
|
|
2445
|
+
throw new Error("Resource URI is missing the note slug.");
|
|
2446
|
+
}
|
|
2447
|
+
return {
|
|
2448
|
+
contents: [{
|
|
2449
|
+
uri: uri.toString(),
|
|
2450
|
+
text: runtime.readNoteMarkdown(decodeURIComponent(slug)),
|
|
2451
|
+
mimeType: "text/markdown"
|
|
2452
|
+
}]
|
|
2453
|
+
};
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
function registerPrompts(server, runtime) {
|
|
2457
|
+
server.registerPrompt("granite_refine_note", {
|
|
2458
|
+
title: "Refine Granite Note",
|
|
2459
|
+
description: "Create a prompt for turning a note into a cleaner permanent note draft.",
|
|
2460
|
+
argsSchema: {
|
|
2461
|
+
slug: z.string().describe("Slug of the note to refine.")
|
|
2462
|
+
}
|
|
2463
|
+
}, async ({ slug }) => {
|
|
2464
|
+
const note = runtime.getNote(slug);
|
|
2465
|
+
return {
|
|
2466
|
+
description: `Refine ${note.slug} into a durable Granite note.`,
|
|
2467
|
+
messages: [
|
|
2468
|
+
{
|
|
2469
|
+
role: "user",
|
|
2470
|
+
content: {
|
|
2471
|
+
type: "text",
|
|
2472
|
+
text: [
|
|
2473
|
+
"Refine the attached Granite note into a durable, well-structured permanent note.",
|
|
2474
|
+
"Keep the meaning intact, avoid inventing facts, preserve useful wikilinks, and use Granite-style headings when appropriate."
|
|
2475
|
+
].join(" ")
|
|
2476
|
+
}
|
|
2477
|
+
},
|
|
2478
|
+
{
|
|
2479
|
+
role: "user",
|
|
2480
|
+
content: {
|
|
2481
|
+
type: "resource",
|
|
2482
|
+
resource: {
|
|
2483
|
+
uri: note.resource_uri,
|
|
2484
|
+
text: runtime.readNoteMarkdown(slug),
|
|
2485
|
+
mimeType: "text/markdown"
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
]
|
|
2490
|
+
};
|
|
2491
|
+
});
|
|
2492
|
+
server.registerPrompt("granite_review_connections", {
|
|
2493
|
+
title: "Review Granite Connections",
|
|
2494
|
+
description: "Create a prompt for improving note links, tags, and next steps.",
|
|
2495
|
+
argsSchema: {
|
|
2496
|
+
slug: z.string().describe("Slug of the note to inspect.")
|
|
2497
|
+
}
|
|
2498
|
+
}, async ({ slug }) => {
|
|
2499
|
+
const note = runtime.getNote(slug);
|
|
2500
|
+
const backlinks = runtime.getBacklinks(slug);
|
|
2501
|
+
const recommendations = runtime.recommend(slug);
|
|
2502
|
+
return {
|
|
2503
|
+
description: `Review the connections around ${note.slug}.`,
|
|
2504
|
+
messages: [
|
|
2505
|
+
{
|
|
2506
|
+
role: "user",
|
|
2507
|
+
content: {
|
|
2508
|
+
type: "text",
|
|
2509
|
+
text: [
|
|
2510
|
+
"Review this Granite note and propose better links, tags, and the most useful follow-up note.",
|
|
2511
|
+
"Prefer concrete suggestions grounded in the vault context below."
|
|
2512
|
+
].join(" ")
|
|
2513
|
+
}
|
|
2514
|
+
},
|
|
2515
|
+
{
|
|
2516
|
+
role: "user",
|
|
2517
|
+
content: {
|
|
2518
|
+
type: "resource",
|
|
2519
|
+
resource: {
|
|
2520
|
+
uri: note.resource_uri,
|
|
2521
|
+
text: runtime.readNoteMarkdown(slug),
|
|
2522
|
+
mimeType: "text/markdown"
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
},
|
|
2526
|
+
{
|
|
2527
|
+
role: "user",
|
|
2528
|
+
content: {
|
|
2529
|
+
type: "text",
|
|
2530
|
+
text: JSON.stringify({ backlinks, recommendations }, null, 2)
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
]
|
|
2534
|
+
};
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
function toolResult(structuredContent, summary) {
|
|
2538
|
+
return {
|
|
2539
|
+
content: [{ type: "text", text: summary }],
|
|
2540
|
+
structuredContent: asStructuredContent(structuredContent)
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
function createNoteResourceLink(name, uri) {
|
|
2544
|
+
return {
|
|
2545
|
+
type: "resource_link",
|
|
2546
|
+
name,
|
|
2547
|
+
uri,
|
|
2548
|
+
mimeType: "text/markdown",
|
|
2549
|
+
description: "Read the markdown resource for this note."
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
function recommendationSummary(recommendations) {
|
|
2553
|
+
return [
|
|
2554
|
+
`${recommendations.links.length} link suggestion(s)`,
|
|
2555
|
+
`${recommendations.tags.length} tag suggestion(s)`,
|
|
2556
|
+
`${recommendations.next_steps.length} next-step suggestion(s)`
|
|
2557
|
+
].join(", ");
|
|
2558
|
+
}
|
|
2559
|
+
function asStructuredContent(value) {
|
|
2560
|
+
return JSON.parse(JSON.stringify(value));
|
|
2561
|
+
}
|
|
2562
|
+
function createGraniteMcpHttpApp(runtime, options) {
|
|
2563
|
+
const app = new Hono2();
|
|
2564
|
+
const allowedHosts = buildAllowedHosts(options.host, options.port);
|
|
2565
|
+
const allowedOrigins = buildAllowedOrigins(options.host, options.port, options.allowedOrigins ?? []);
|
|
2566
|
+
app.use("/mcp", async (c, next) => {
|
|
2567
|
+
const requestHost = (c.req.header("host") ?? "").toLowerCase();
|
|
2568
|
+
if (allowedHosts.size > 0 && !allowedHosts.has(requestHost)) {
|
|
2569
|
+
return c.json({
|
|
2570
|
+
jsonrpc: "2.0",
|
|
2571
|
+
error: { code: -32e3, message: "Forbidden host header." },
|
|
2572
|
+
id: null
|
|
2573
|
+
}, 403);
|
|
2574
|
+
}
|
|
2575
|
+
const origin = c.req.header("origin");
|
|
2576
|
+
if (origin && !allowedOrigins.has(origin)) {
|
|
2577
|
+
return c.json({
|
|
2578
|
+
jsonrpc: "2.0",
|
|
2579
|
+
error: { code: -32e3, message: "Forbidden origin." },
|
|
2580
|
+
id: null
|
|
2581
|
+
}, 403);
|
|
2582
|
+
}
|
|
2583
|
+
if (c.req.method === "OPTIONS") {
|
|
2584
|
+
return new Response(null, {
|
|
2585
|
+
status: 204,
|
|
2586
|
+
headers: corsHeaders(origin, allowedOrigins)
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
await next();
|
|
2590
|
+
});
|
|
2591
|
+
app.get("/health", (c) => c.json({ status: "ok", name: "granite-mcp", version: GRANITE_VERSION }));
|
|
2592
|
+
app.all("/mcp", async (c) => {
|
|
2593
|
+
const origin = c.req.header("origin");
|
|
2594
|
+
const server = createGraniteMcpServer(runtime);
|
|
2595
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
2596
|
+
sessionIdGenerator: void 0,
|
|
2597
|
+
enableJsonResponse: options.jsonResponse ?? false
|
|
2598
|
+
});
|
|
2599
|
+
try {
|
|
2600
|
+
await server.connect(transport);
|
|
2601
|
+
const response = await transport.handleRequest(c.req.raw);
|
|
2602
|
+
const managedResponse = await withResponseCleanup(response, async () => {
|
|
2603
|
+
await server.close();
|
|
2604
|
+
});
|
|
2605
|
+
return withCors(managedResponse, origin, allowedOrigins);
|
|
2606
|
+
} catch (error) {
|
|
2607
|
+
await server.close();
|
|
2608
|
+
throw error;
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
return app;
|
|
2612
|
+
}
|
|
2613
|
+
function buildAllowedHosts(host, port) {
|
|
2614
|
+
const normalizedHost = host.toLowerCase();
|
|
2615
|
+
if (normalizedHost === "0.0.0.0" || normalizedHost === "::" || normalizedHost === "[::]") {
|
|
2616
|
+
return /* @__PURE__ */ new Set();
|
|
2617
|
+
}
|
|
2618
|
+
const allowed = /* @__PURE__ */ new Set([`${normalizedHost}:${port}`]);
|
|
2619
|
+
if (normalizedHost === "127.0.0.1" || normalizedHost === "localhost") {
|
|
2620
|
+
allowed.add(`127.0.0.1:${port}`);
|
|
2621
|
+
allowed.add(`localhost:${port}`);
|
|
2622
|
+
}
|
|
2623
|
+
if (normalizedHost === "::1" || normalizedHost === "[::1]") {
|
|
2624
|
+
allowed.add(`[::1]:${port}`);
|
|
2625
|
+
}
|
|
2626
|
+
return allowed;
|
|
2627
|
+
}
|
|
2628
|
+
function buildAllowedOrigins(host, port, extraOrigins) {
|
|
2629
|
+
const allowed = new Set(extraOrigins);
|
|
2630
|
+
const normalizedHost = host.toLowerCase();
|
|
2631
|
+
if (normalizedHost === "::1" || normalizedHost === "[::1]") {
|
|
2632
|
+
allowed.add(`http://[::1]:${port}`);
|
|
2633
|
+
} else if (normalizedHost !== "0.0.0.0" && normalizedHost !== "::" && normalizedHost !== "[::]") {
|
|
2634
|
+
allowed.add(`http://${normalizedHost}:${port}`);
|
|
2635
|
+
}
|
|
2636
|
+
if (normalizedHost === "127.0.0.1" || normalizedHost === "localhost") {
|
|
2637
|
+
allowed.add(`http://127.0.0.1:${port}`);
|
|
2638
|
+
allowed.add(`http://localhost:${port}`);
|
|
2639
|
+
}
|
|
2640
|
+
return allowed;
|
|
2641
|
+
}
|
|
2642
|
+
function withCors(response, origin, allowedOrigins) {
|
|
2643
|
+
const headers = new Headers(response.headers);
|
|
2644
|
+
for (const [key, value] of corsHeaders(origin, allowedOrigins)) {
|
|
2645
|
+
headers.set(key, value);
|
|
2646
|
+
}
|
|
2647
|
+
return new Response(response.body, {
|
|
2648
|
+
status: response.status,
|
|
2649
|
+
statusText: response.statusText,
|
|
2650
|
+
headers
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
function corsHeaders(origin, allowedOrigins) {
|
|
2654
|
+
const headers = new Headers();
|
|
2655
|
+
if (origin && allowedOrigins.has(origin)) {
|
|
2656
|
+
headers.set("Access-Control-Allow-Origin", origin);
|
|
2657
|
+
headers.set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
2658
|
+
headers.set("Access-Control-Allow-Headers", "Content-Type, MCP-Protocol-Version, Last-Event-ID");
|
|
2659
|
+
headers.set("Access-Control-Expose-Headers", "MCP-Session-Id, MCP-Protocol-Version");
|
|
2660
|
+
headers.set("Vary", "Origin");
|
|
2661
|
+
}
|
|
2662
|
+
return headers;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// src/mcp/runtime.ts
|
|
2666
|
+
import fs12 from "fs";
|
|
2667
|
+
import path8 from "path";
|
|
2668
|
+
var GraniteMcpRuntime = class {
|
|
2669
|
+
vaultRoot;
|
|
2670
|
+
config;
|
|
2671
|
+
db;
|
|
2672
|
+
indexCheckIntervalMs;
|
|
2673
|
+
lastSignature;
|
|
2674
|
+
lastIndexCheckAt = 0;
|
|
2675
|
+
constructor(vaultRoot, options = {}) {
|
|
2676
|
+
this.vaultRoot = path8.resolve(vaultRoot);
|
|
2677
|
+
this.config = loadConfig(this.vaultRoot);
|
|
2678
|
+
this.db = openDatabase(this.vaultRoot);
|
|
2679
|
+
this.indexCheckIntervalMs = options.indexCheckIntervalMs ?? 1500;
|
|
2680
|
+
}
|
|
2681
|
+
close() {
|
|
2682
|
+
this.db.close();
|
|
2683
|
+
}
|
|
2684
|
+
getVaultOverview(recentLimit = 10) {
|
|
2685
|
+
const notes = this.readAllNotes().sort((a, b) => b.frontmatter.modified.localeCompare(a.frontmatter.modified));
|
|
2686
|
+
const byType = {};
|
|
2687
|
+
for (const note of notes) {
|
|
2688
|
+
byType[note.frontmatter.type] = (byType[note.frontmatter.type] ?? 0) + 1;
|
|
2689
|
+
}
|
|
2690
|
+
return {
|
|
2691
|
+
vault_root: this.vaultRoot,
|
|
2692
|
+
vault_name: this.config.vault_name,
|
|
2693
|
+
default_type: this.config.defaults.note_type,
|
|
2694
|
+
auto_rebuild: this.config.index.auto_rebuild,
|
|
2695
|
+
index_last_rebuild: this.getMetaValue("last_rebuild"),
|
|
2696
|
+
note_count: notes.length,
|
|
2697
|
+
notes_by_type: byType,
|
|
2698
|
+
recent_notes: notes.slice(0, clampLimit(recentLimit, 20)).map((note) => this.toNoteSummary(note))
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
listNoteTypes() {
|
|
2702
|
+
return Object.entries(this.config.note_types).map(([name, typeConfig]) => ({
|
|
2703
|
+
name,
|
|
2704
|
+
description: typeConfig.description,
|
|
2705
|
+
folder: typeConfig.folder,
|
|
2706
|
+
line_limit: typeConfig.line_limit,
|
|
2707
|
+
warn_only: typeConfig.warn_only,
|
|
2708
|
+
slug_format: typeConfig.slug_format ?? "title",
|
|
2709
|
+
instructions: typeConfig.instructions,
|
|
2710
|
+
fields: typeConfig.fields
|
|
2711
|
+
}));
|
|
2712
|
+
}
|
|
2713
|
+
getDefaultNoteType() {
|
|
2714
|
+
return this.config.defaults.note_type;
|
|
2715
|
+
}
|
|
2716
|
+
listNotes(input = {}) {
|
|
2717
|
+
let notes = this.readAllNotes();
|
|
2718
|
+
if (input.type) {
|
|
2719
|
+
notes = notes.filter((note) => note.frontmatter.type === input.type);
|
|
2720
|
+
}
|
|
2721
|
+
if (input.status) {
|
|
2722
|
+
notes = notes.filter((note) => note.frontmatter.status === input.status);
|
|
2723
|
+
}
|
|
2724
|
+
if (input.source) {
|
|
2725
|
+
notes = notes.filter((note) => note.frontmatter.source === input.source);
|
|
2726
|
+
}
|
|
2727
|
+
if (input.since) {
|
|
2728
|
+
const since = input.since;
|
|
2729
|
+
notes = notes.filter((note) => note.frontmatter.modified >= since);
|
|
2730
|
+
}
|
|
2731
|
+
notes.sort((a, b) => b.frontmatter.modified.localeCompare(a.frontmatter.modified));
|
|
2732
|
+
return notes.slice(0, clampLimit(input.limit ?? 25, 200)).map((note) => this.toNoteSummary(note));
|
|
2733
|
+
}
|
|
2734
|
+
getNote(slug) {
|
|
2735
|
+
const note = this.requireNote(slug);
|
|
2736
|
+
const allNotes = this.readAllNotes();
|
|
2737
|
+
const outgoingLinks = resolveWikilinks(parseWikilinks(note.body), allNotes);
|
|
2738
|
+
return {
|
|
2739
|
+
...this.toNoteSummary(note),
|
|
2740
|
+
body: note.body,
|
|
2741
|
+
frontmatter: note.frontmatter,
|
|
2742
|
+
outgoing_links: outgoingLinks
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
search(query, limit = 10) {
|
|
2746
|
+
this.refreshIndex();
|
|
2747
|
+
return searchNotes(this.db, query, clampLimit(limit, 50));
|
|
2748
|
+
}
|
|
2749
|
+
getBacklinks(slug) {
|
|
2750
|
+
this.requireNote(slug);
|
|
2751
|
+
this.refreshIndex();
|
|
2752
|
+
return getBacklinks(this.db, slug);
|
|
2753
|
+
}
|
|
2754
|
+
suggestLinks(slug) {
|
|
2755
|
+
const note = this.requireNote(slug);
|
|
2756
|
+
this.refreshIndex();
|
|
2757
|
+
return suggestLinks(this.db, note);
|
|
2758
|
+
}
|
|
2759
|
+
recommend(slug) {
|
|
2760
|
+
const note = this.requireNote(slug);
|
|
2761
|
+
this.refreshIndex();
|
|
2762
|
+
return getRecommendations(this.db, note, this.config);
|
|
2763
|
+
}
|
|
2764
|
+
runDoctor() {
|
|
2765
|
+
this.refreshIndex();
|
|
2766
|
+
const issues = runDoctor(this.vaultRoot, this.config, this.db);
|
|
2767
|
+
return {
|
|
2768
|
+
counts: {
|
|
2769
|
+
errors: issues.filter((issue) => issue.level === "error").length,
|
|
2770
|
+
warnings: issues.filter((issue) => issue.level === "warning").length,
|
|
2771
|
+
info: issues.filter((issue) => issue.level === "info").length
|
|
2772
|
+
},
|
|
2773
|
+
issues
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
createNote(input) {
|
|
2777
|
+
const resolvedType = input.type ?? this.config.defaults.note_type;
|
|
2778
|
+
const typeConfig = this.config.note_types[resolvedType];
|
|
2779
|
+
if (!typeConfig) {
|
|
2780
|
+
throw new Error(`Unknown note type: "${resolvedType}"`);
|
|
2781
|
+
}
|
|
2782
|
+
const bodyOverride = input.body !== void 0 ? ensureTrailingNewline(input.body) : typeConfig.slug_format === "date" ? ensureTrailingNewline(input.title) : void 0;
|
|
2783
|
+
const created = createNote(this.vaultRoot, this.config, resolvedType, input.title, bodyOverride);
|
|
2784
|
+
const metadataMutations = {
|
|
2785
|
+
tags: input.tags,
|
|
2786
|
+
aliases: input.aliases,
|
|
2787
|
+
status: input.status,
|
|
2788
|
+
source: input.source
|
|
2789
|
+
};
|
|
2790
|
+
if (hasMetadataMutations(metadataMutations)) {
|
|
2791
|
+
this.applyMutations(created.filepath, metadataMutations);
|
|
2792
|
+
}
|
|
2793
|
+
return this.afterWrite(created.slug, true);
|
|
2794
|
+
}
|
|
2795
|
+
captureNote(input) {
|
|
2796
|
+
const content = input.text.trim();
|
|
2797
|
+
if (!content) {
|
|
2798
|
+
throw new Error("Capture text cannot be empty.");
|
|
2799
|
+
}
|
|
2800
|
+
const firstLine = content.split("\n")[0] ?? "Untitled";
|
|
2801
|
+
const title = firstLine.length > 60 ? `${firstLine.slice(0, 60).trim()}...` : firstLine;
|
|
2802
|
+
return this.createNote({
|
|
2803
|
+
title,
|
|
2804
|
+
type: input.type,
|
|
2805
|
+
body: ensureTrailingNewline(content),
|
|
2806
|
+
tags: input.tags,
|
|
2807
|
+
aliases: input.aliases,
|
|
2808
|
+
status: input.status,
|
|
2809
|
+
source: input.source
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
updateNote(slug, input) {
|
|
2813
|
+
const note = this.requireNote(slug);
|
|
2814
|
+
const needsFullRebuild = input.title !== void 0 || (input.aliases?.length ?? 0) > 0;
|
|
2815
|
+
this.applyMutations(note.filepath, input);
|
|
2816
|
+
if (needsFullRebuild) {
|
|
2817
|
+
return this.afterWrite(slug, true);
|
|
2818
|
+
}
|
|
2819
|
+
this.refreshIndex();
|
|
2820
|
+
const updated = readNote(note.filepath);
|
|
2821
|
+
syncNoteInIndex(this.vaultRoot, this.config, this.db, updated);
|
|
2822
|
+
this.captureSignature();
|
|
2823
|
+
return {
|
|
2824
|
+
note: this.getNote(updated.slug),
|
|
2825
|
+
recommendations: getRecommendations(this.db, updated, this.config)
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
readVaultConfigRaw() {
|
|
2829
|
+
return fs12.readFileSync(path8.join(this.vaultRoot, CONFIG_FILENAME), "utf-8");
|
|
2830
|
+
}
|
|
2831
|
+
readVaultTypesJson() {
|
|
2832
|
+
return JSON.stringify({
|
|
2833
|
+
default_type: this.config.defaults.note_type,
|
|
2834
|
+
note_types: this.listNoteTypes()
|
|
2835
|
+
}, null, 2);
|
|
2836
|
+
}
|
|
2837
|
+
readVaultOverviewJson() {
|
|
2838
|
+
return JSON.stringify(this.getVaultOverview(), null, 2);
|
|
2839
|
+
}
|
|
2840
|
+
readNoteMarkdown(slug) {
|
|
2841
|
+
const note = this.requireNote(slug);
|
|
2842
|
+
return fs12.readFileSync(note.filepath, "utf-8");
|
|
2843
|
+
}
|
|
2844
|
+
completeSlugs(prefix = "") {
|
|
2845
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
2846
|
+
return this.readAllNotes().sort((a, b) => b.frontmatter.modified.localeCompare(a.frontmatter.modified)).map((note) => note.slug).filter((slug) => slug.toLowerCase().startsWith(normalizedPrefix)).slice(0, 25);
|
|
2847
|
+
}
|
|
2848
|
+
noteResourceUri(slug) {
|
|
2849
|
+
return `granite://notes/${encodeURIComponent(slug)}`;
|
|
2850
|
+
}
|
|
2851
|
+
refreshIndex(force = false) {
|
|
2852
|
+
const now = Date.now();
|
|
2853
|
+
if (!force && now - this.lastIndexCheckAt < this.indexCheckIntervalMs) {
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
const currentConfigMtimeMs = this.getConfigMtimeMs();
|
|
2857
|
+
if (!this.lastSignature || currentConfigMtimeMs !== this.lastSignature.configMtimeMs) {
|
|
2858
|
+
this.config = loadConfig(this.vaultRoot);
|
|
2859
|
+
}
|
|
2860
|
+
const signature = this.computeSignature();
|
|
2861
|
+
let shouldRebuild = false;
|
|
2862
|
+
if (force) {
|
|
2863
|
+
shouldRebuild = true;
|
|
2864
|
+
} else if (this.config.index.auto_rebuild) {
|
|
2865
|
+
if (!this.lastSignature) {
|
|
2866
|
+
shouldRebuild = signature.noteCount !== this.getIndexedNoteCount() || signature.latestMutationMs > this.getLastRebuildMs();
|
|
2867
|
+
} else {
|
|
2868
|
+
shouldRebuild = signature.noteCount !== this.lastSignature.noteCount || signature.latestMutationMs !== this.lastSignature.latestMutationMs || signature.configMtimeMs !== this.lastSignature.configMtimeMs;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
if (shouldRebuild) {
|
|
2872
|
+
rebuildIndex(this.vaultRoot, this.config, this.db);
|
|
2873
|
+
}
|
|
2874
|
+
this.lastSignature = signature;
|
|
2875
|
+
this.lastIndexCheckAt = now;
|
|
2876
|
+
}
|
|
2877
|
+
readAllNotes() {
|
|
2878
|
+
return listNotes(this.vaultRoot, this.config);
|
|
2879
|
+
}
|
|
2880
|
+
requireNote(slug) {
|
|
2881
|
+
const note = findNoteBySlug(this.vaultRoot, this.config, slug);
|
|
2882
|
+
if (!note) {
|
|
2883
|
+
throw new Error(`Note not found: ${slug}`);
|
|
2884
|
+
}
|
|
2885
|
+
return note;
|
|
2886
|
+
}
|
|
2887
|
+
toNoteSummary(note) {
|
|
2888
|
+
return {
|
|
2889
|
+
slug: note.slug,
|
|
2890
|
+
title: note.frontmatter.title,
|
|
2891
|
+
type: note.frontmatter.type,
|
|
2892
|
+
created: note.frontmatter.created,
|
|
2893
|
+
modified: note.frontmatter.modified,
|
|
2894
|
+
tags: note.frontmatter.tags,
|
|
2895
|
+
aliases: note.frontmatter.aliases,
|
|
2896
|
+
status: note.frontmatter.status,
|
|
2897
|
+
source: note.frontmatter.source,
|
|
2898
|
+
filepath: note.filepath,
|
|
2899
|
+
resource_uri: this.noteResourceUri(note.slug)
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
applyMutations(filepath, input) {
|
|
2903
|
+
const raw = fs12.readFileSync(filepath, "utf-8");
|
|
2904
|
+
const { frontmatter, body: existingBody } = parseFrontmatter(raw);
|
|
2905
|
+
let body = existingBody;
|
|
2906
|
+
if (input.title !== void 0) {
|
|
2907
|
+
frontmatter.title = input.title;
|
|
2908
|
+
}
|
|
2909
|
+
if (input.tags && input.tags.length > 0) {
|
|
2910
|
+
frontmatter.tags = mergeUnique(frontmatter.tags, input.tags);
|
|
2911
|
+
}
|
|
2912
|
+
if (input.aliases && input.aliases.length > 0) {
|
|
2913
|
+
frontmatter.aliases = mergeUnique(frontmatter.aliases, input.aliases);
|
|
2914
|
+
}
|
|
2915
|
+
if (input.status !== void 0) {
|
|
2916
|
+
validateStatus(input.status);
|
|
2917
|
+
frontmatter.status = input.status;
|
|
2918
|
+
}
|
|
2919
|
+
if (input.source !== void 0) {
|
|
2920
|
+
validateSource(input.source);
|
|
2921
|
+
frontmatter.source = input.source;
|
|
2922
|
+
}
|
|
2923
|
+
if (input.body !== void 0) {
|
|
2924
|
+
body = ensureTrailingNewline(input.body);
|
|
2925
|
+
}
|
|
2926
|
+
if (input.append !== void 0) {
|
|
2927
|
+
body = `${body.trimEnd()}
|
|
2928
|
+
${input.append}
|
|
2929
|
+
`;
|
|
2930
|
+
}
|
|
2931
|
+
frontmatter.modified = (/* @__PURE__ */ new Date()).toISOString();
|
|
2932
|
+
fs12.writeFileSync(filepath, serializeFrontmatter(frontmatter, body), "utf-8");
|
|
2933
|
+
}
|
|
2934
|
+
afterWrite(slug, fullRebuild) {
|
|
2935
|
+
if (fullRebuild) {
|
|
2936
|
+
this.refreshIndex(true);
|
|
2937
|
+
} else {
|
|
2938
|
+
this.refreshIndex();
|
|
2939
|
+
const note2 = this.requireNote(slug);
|
|
2940
|
+
syncNoteInIndex(this.vaultRoot, this.config, this.db, note2);
|
|
2941
|
+
this.captureSignature();
|
|
2942
|
+
}
|
|
2943
|
+
const note = this.requireNote(slug);
|
|
2944
|
+
return {
|
|
2945
|
+
note: this.getNote(note.slug),
|
|
2946
|
+
recommendations: getRecommendations(this.db, note, this.config)
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
captureSignature() {
|
|
2950
|
+
this.lastSignature = this.computeSignature();
|
|
2951
|
+
this.lastIndexCheckAt = Date.now();
|
|
2952
|
+
}
|
|
2953
|
+
computeSignature() {
|
|
2954
|
+
let noteCount = 0;
|
|
2955
|
+
let latestNoteMtimeMs = 0;
|
|
2956
|
+
for (const typeConfig of Object.values(this.config.note_types)) {
|
|
2957
|
+
const folder = path8.join(this.vaultRoot, typeConfig.folder);
|
|
2958
|
+
if (!fs12.existsSync(folder)) {
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
for (const entry of fs12.readdirSync(folder)) {
|
|
2962
|
+
if (!entry.endsWith(".md")) {
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
noteCount += 1;
|
|
2966
|
+
const stat = fs12.statSync(path8.join(folder, entry));
|
|
2967
|
+
latestNoteMtimeMs = Math.max(latestNoteMtimeMs, stat.mtimeMs);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
const configMtimeMs = this.getConfigMtimeMs();
|
|
2971
|
+
return {
|
|
2972
|
+
noteCount,
|
|
2973
|
+
latestMutationMs: Math.max(latestNoteMtimeMs, configMtimeMs),
|
|
2974
|
+
configMtimeMs
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
getConfigMtimeMs() {
|
|
2978
|
+
const configPath = path8.join(this.vaultRoot, CONFIG_FILENAME);
|
|
2979
|
+
return fs12.existsSync(configPath) ? fs12.statSync(configPath).mtimeMs : 0;
|
|
2980
|
+
}
|
|
2981
|
+
getLastRebuildMs() {
|
|
2982
|
+
const value = this.getMetaValue("last_rebuild");
|
|
2983
|
+
return value ? Date.parse(value) || 0 : 0;
|
|
2984
|
+
}
|
|
2985
|
+
getMetaValue(key) {
|
|
2986
|
+
const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
2987
|
+
return row?.value;
|
|
2988
|
+
}
|
|
2989
|
+
getIndexedNoteCount() {
|
|
2990
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM notes").get();
|
|
2991
|
+
return row.count;
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2994
|
+
function ensureTrailingNewline(value) {
|
|
2995
|
+
return value.endsWith("\n") ? value : `${value}
|
|
2996
|
+
`;
|
|
2997
|
+
}
|
|
2998
|
+
function clampLimit(value, max) {
|
|
2999
|
+
return Math.max(1, Math.min(value, max));
|
|
3000
|
+
}
|
|
3001
|
+
function mergeUnique(existing, incoming) {
|
|
3002
|
+
const merged = new Set(existing);
|
|
3003
|
+
for (const value of incoming) {
|
|
3004
|
+
const trimmed = value.trim();
|
|
3005
|
+
if (trimmed) {
|
|
3006
|
+
merged.add(trimmed);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
return [...merged];
|
|
3010
|
+
}
|
|
3011
|
+
function hasMetadataMutations(input) {
|
|
3012
|
+
return (input.tags?.length ?? 0) > 0 || (input.aliases?.length ?? 0) > 0 || input.status !== void 0 || input.source !== void 0;
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// src/commands/mcp.ts
|
|
3016
|
+
async function mcpCommand(options) {
|
|
3017
|
+
const transport = parseTransport(options.transport);
|
|
3018
|
+
const vaultRoot = resolveVaultRoot(options.vault);
|
|
3019
|
+
const runtime = new GraniteMcpRuntime(vaultRoot);
|
|
3020
|
+
const shutdown = () => {
|
|
3021
|
+
runtime.close();
|
|
3022
|
+
process.exit(0);
|
|
3023
|
+
};
|
|
3024
|
+
process.once("SIGINT", shutdown);
|
|
3025
|
+
process.once("SIGTERM", shutdown);
|
|
3026
|
+
if (transport === "http") {
|
|
3027
|
+
const port = parsePort(options.port);
|
|
3028
|
+
startGraniteMcpHttpServer(runtime, {
|
|
3029
|
+
host: options.host ?? "127.0.0.1",
|
|
3030
|
+
port,
|
|
3031
|
+
allowedOrigins: options.allowOrigin ?? [],
|
|
3032
|
+
jsonResponse: options.jsonResponse ?? false
|
|
3033
|
+
});
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
await startGraniteMcpStdioServer(runtime);
|
|
3037
|
+
}
|
|
3038
|
+
function resolveVaultRoot(explicitVault) {
|
|
3039
|
+
const fromEnv = process.env.GRANITE_VAULT;
|
|
3040
|
+
if (explicitVault) {
|
|
3041
|
+
return path9.resolve(explicitVault);
|
|
3042
|
+
}
|
|
3043
|
+
if (fromEnv) {
|
|
3044
|
+
return path9.resolve(fromEnv);
|
|
3045
|
+
}
|
|
3046
|
+
return requireVaultRoot();
|
|
3047
|
+
}
|
|
3048
|
+
function parseTransport(value) {
|
|
3049
|
+
const transport = value ?? "stdio";
|
|
3050
|
+
if (transport !== "stdio" && transport !== "http") {
|
|
3051
|
+
throw new Error(`Invalid MCP transport: ${transport}. Expected "stdio" or "http".`);
|
|
3052
|
+
}
|
|
3053
|
+
return transport;
|
|
3054
|
+
}
|
|
3055
|
+
function parsePort(value) {
|
|
3056
|
+
const raw = (value ?? process.env.MCP_PORT ?? "3321").trim();
|
|
3057
|
+
if (!/^\d+$/.test(raw)) {
|
|
3058
|
+
throw new Error(`Invalid MCP port: ${raw}`);
|
|
3059
|
+
}
|
|
3060
|
+
const parsed = Number.parseInt(raw, 10);
|
|
3061
|
+
if (parsed <= 0) {
|
|
3062
|
+
throw new Error(`Invalid MCP port: ${raw}`);
|
|
3063
|
+
}
|
|
3064
|
+
return parsed;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
1945
3067
|
// src/index.ts
|
|
1946
3068
|
var program = new Command();
|
|
1947
|
-
program.name("mem").description("Granite \u2014 a local-first markdown memory system").version(
|
|
1948
|
-
program.command("init").description("Initialize
|
|
1949
|
-
initVault(
|
|
3069
|
+
program.name("mem").description("Granite \u2014 a local-first markdown memory system").version(GRANITE_VERSION);
|
|
3070
|
+
program.command("init").description("Initialize the default vault in ~/.granite").action(() => {
|
|
3071
|
+
initVault();
|
|
1950
3072
|
});
|
|
1951
3073
|
program.command("new").description("Create a new note").argument("<title>", "Note title").option("-t, --type <type>", "Note type (e.g. fleeting, permanent, reference)").option("--source <source>", "Set source (human, agent, extraction)").option("--status <status>", "Set status (inbox, active, archived)").option("--json", "Output as JSON (agent-friendly)").action((title, options) => {
|
|
1952
3074
|
newNote(title, options);
|
|
@@ -1987,4 +3109,17 @@ program.command("doctor").description("Validate vault health").action(() => {
|
|
|
1987
3109
|
program.command("serve").description("Start the local web UI").option("-p, --port <port>", "Port number", "4321").action((options) => {
|
|
1988
3110
|
serveCommand(options);
|
|
1989
3111
|
});
|
|
1990
|
-
program.
|
|
3112
|
+
program.command("mcp").description("Start Granite as an MCP server").option("--vault <path>", "Vault root. Defaults to the current Granite vault or $GRANITE_VAULT").option("--transport <transport>", "Transport to use: stdio or http", parseTransportOption, "stdio").option("--host <host>", "Host for HTTP transport", "127.0.0.1").option("--port <port>", "Port for HTTP transport", "3321").option("--allow-origin <origin>", "Allow an HTTP Origin for browser-based HTTP clients", collectValues, []).option("--json-response", "Prefer JSON HTTP responses instead of SSE streams").action(async (options) => {
|
|
3113
|
+
await mcpCommand(options);
|
|
3114
|
+
});
|
|
3115
|
+
await program.parseAsync();
|
|
3116
|
+
function collectValues(value, previous) {
|
|
3117
|
+
return [...previous, value];
|
|
3118
|
+
}
|
|
3119
|
+
function parseTransportOption(value) {
|
|
3120
|
+
try {
|
|
3121
|
+
return parseTransport(value);
|
|
3122
|
+
} catch (error) {
|
|
3123
|
+
throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
|
|
3124
|
+
}
|
|
3125
|
+
}
|