@hiveforge/hivemind-mcp 2.0.0 → 2.1.1
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 +58 -26
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +421 -1
- package/dist/server.js.map +1 -1
- package/dist/types/index.d.ts +323 -472
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +31 -8
- package/dist/types/index.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
# Hivemind MCP Server
|
|
6
6
|
|
|
7
|
-
[](https://www.npmjs.com/package/@hiveforge/hivemind-mcp)
|
|
8
|
+
[](https://github.com/hiveforge-io/hivemind/actions/workflows/test.yml)
|
|
9
|
+
[](https://github.com/hiveforge-io/hivemind/actions/workflows/release.yml)
|
|
10
|
+
[](https://codecov.io/gh/hiveforge-io/hivemind)
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[](https://github.com/hiveforge-io/hivemind/issues)
|
|
13
|
+
[](https://github.com/hiveforge-io/hivemind/stargazers)
|
|
8
14
|
|
|
9
15
|
An MCP (Model Context Protocol) server for Obsidian worldbuilding vaults that provides AI tools with consistent, canonical context from your fictional worlds.
|
|
10
16
|
|
|
@@ -134,39 +140,65 @@ Obsidian Vault → File Watcher → Markdown Parser → Knowledge Graph
|
|
|
134
140
|
|
|
135
141
|
## Development Status
|
|
136
142
|
|
|
137
|
-
**Current Phase**:
|
|
143
|
+
**Current Phase**: Milestone 1.0 Complete ✅
|
|
138
144
|
|
|
139
|
-
###
|
|
145
|
+
### What's Included
|
|
140
146
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
- [x]
|
|
145
|
-
- [x] Vault reading and file watching (VaultReader, VaultWatcher)
|
|
146
|
-
- [x] Markdown parsing with wikilinks (MarkdownParser)
|
|
147
|
-
- [x] Knowledge graph construction (GraphBuilder, HivemindDatabase)
|
|
148
|
-
- [x] HybridRAG search implementation (SearchEngine)
|
|
149
|
-
- [x] MCP tools (query_character, query_location, search_vault, rebuild_index)
|
|
150
|
-
- [x] CLI vault override flag (--vault)
|
|
151
|
-
- [x] Automatic stale index detection on startup
|
|
147
|
+
- [x] MCP server with hybrid search (vector, graph, keyword)
|
|
148
|
+
- [x] Vault templates for all entity types (Character, Location, Event, Faction, Lore, Asset)
|
|
149
|
+
- [x] Canon workflow tools (status management, consistency validation)
|
|
150
|
+
- [x] Asset management with full provenance tracking
|
|
152
151
|
- [x] ComfyUI integration with workflow management
|
|
153
|
-
- [x] Obsidian plugin with image generation
|
|
154
|
-
- [x]
|
|
155
|
-
- [x] Test coverage improvement (37% → 45%)
|
|
156
|
-
- [x] CodeQL security scanning integration
|
|
152
|
+
- [x] Obsidian plugin with image generation
|
|
153
|
+
- [x] CI/CD with semantic-release and CodeQL scanning
|
|
157
154
|
|
|
158
155
|
**Up Next:**
|
|
159
|
-
- [ ] Obsidian plugin
|
|
160
|
-
- [ ]
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
- [ ] Obsidian community plugin submission
|
|
157
|
+
- [ ] Template System (make Hivemind domain-agnostic)
|
|
158
|
+
|
|
159
|
+
## MCP Tools
|
|
160
|
+
|
|
161
|
+
### Query Tools
|
|
162
|
+
| Tool | Description |
|
|
163
|
+
|------|-------------|
|
|
164
|
+
| `query_character` | Get character with relationships and content |
|
|
165
|
+
| `query_location` | Get location with hierarchy and connected entities |
|
|
166
|
+
| `search_vault` | Hybrid search across all content with filters |
|
|
167
|
+
|
|
168
|
+
### Asset Management
|
|
169
|
+
| Tool | Description |
|
|
170
|
+
|------|-------------|
|
|
171
|
+
| `store_asset` | Store generated image with provenance metadata |
|
|
172
|
+
| `query_asset` | Get asset with generation settings |
|
|
173
|
+
| `list_assets` | Filter assets by entity, type, status, workflow |
|
|
174
|
+
|
|
175
|
+
### Canon Workflow
|
|
176
|
+
| Tool | Description |
|
|
177
|
+
|------|-------------|
|
|
178
|
+
| `get_canon_status` | List entities grouped by status (draft/pending/canon) |
|
|
179
|
+
| `submit_for_review` | Move entity from draft to pending review |
|
|
180
|
+
| `validate_consistency` | Check for broken links, duplicates, conflicts |
|
|
181
|
+
|
|
182
|
+
### ComfyUI Integration (when enabled)
|
|
183
|
+
| Tool | Description |
|
|
184
|
+
|------|-------------|
|
|
185
|
+
| `store_workflow` | Save ComfyUI workflow to vault |
|
|
186
|
+
| `list_workflows` | Browse saved workflows |
|
|
187
|
+
| `get_workflow` | Retrieve workflow by ID |
|
|
188
|
+
| `generate_image` | Generate image with vault context injection |
|
|
189
|
+
|
|
190
|
+
### Utility
|
|
191
|
+
| Tool | Description |
|
|
192
|
+
|------|-------------|
|
|
193
|
+
| `rebuild_index` | Force complete re-index of vault |
|
|
194
|
+
| `get_vault_stats` | Vault statistics and token savings metrics |
|
|
163
195
|
|
|
164
196
|
## Documentation
|
|
165
197
|
|
|
166
|
-
- [
|
|
167
|
-
- [
|
|
168
|
-
- [
|
|
169
|
-
- [
|
|
198
|
+
- [Setup Guide](docs/SETUP_GUIDE.md)
|
|
199
|
+
- [ComfyUI Integration](docs/COMFYUI_INTEGRATION.md)
|
|
200
|
+
- [Obsidian Plugin Workflow](docs/OBSIDIAN_PLUGIN_WORKFLOW.md)
|
|
201
|
+
- [Vault Templates](sample-vault/Templates/README.md)
|
|
170
202
|
|
|
171
203
|
## License
|
|
172
204
|
|
package/dist/server.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export declare class HivemindServer {
|
|
|
28
28
|
private handleGetWorkflow;
|
|
29
29
|
private handleGenerateImage;
|
|
30
30
|
private handleStoreAsset;
|
|
31
|
+
private handleQueryAsset;
|
|
32
|
+
private handleListAssets;
|
|
33
|
+
private handleGetCanonStatus;
|
|
34
|
+
private handleSubmitForReview;
|
|
35
|
+
private handleValidateConsistency;
|
|
31
36
|
start(): Promise<void>;
|
|
32
37
|
private printReadyBanner;
|
|
33
38
|
}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AASA,OAAO,EAYL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAY1B,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,YAAY,CAIlB;gBAEU,MAAM,EAAE,cAAc;IA+ClC,OAAO,CAAC,aAAa;YA0fP,oBAAoB;YA+CpB,mBAAmB;YA+CnB,iBAAiB;YA2CjB,kBAAkB;YAiClB,mBAAmB;IA6FjC,OAAO,CAAC,iBAAiB;YAcX,aAAa;IAwC3B,OAAO,CAAC,gCAAgC;IAwExC,OAAO,CAAC,+BAA+B;IA8CvC,OAAO,CAAC,mBAAmB;YA2Bb,mBAAmB;YAkBnB,mBAAmB;YA8BnB,iBAAiB;YA0BjB,mBAAmB;YAwInB,gBAAgB;YAkChB,gBAAgB;YAkDhB,gBAAgB;YA4DhB,oBAAoB;YAqEpB,qBAAqB;YAsDrB,yBAAyB;IA4GjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAkCd,gBAAgB;CA2B/B"}
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
-
import { QueryCharacterArgsSchema, QueryLocationArgsSchema, SearchVaultArgsSchema, StoreWorkflowArgsSchema, GenerateImageArgsSchema, StoreAssetArgsSchema, } from './types/index.js';
|
|
4
|
+
import { QueryCharacterArgsSchema, QueryLocationArgsSchema, SearchVaultArgsSchema, StoreWorkflowArgsSchema, GenerateImageArgsSchema, StoreAssetArgsSchema, QueryAssetArgsSchema, ListAssetsArgsSchema, GetCanonStatusArgsSchema, SubmitForReviewArgsSchema, ValidateConsistencyArgsSchema, } from './types/index.js';
|
|
5
5
|
import { VaultReader } from './vault/reader.js';
|
|
6
6
|
import { VaultWatcher } from './vault/watcher.js';
|
|
7
7
|
import { HivemindDatabase } from './graph/database.js';
|
|
@@ -188,6 +188,110 @@ export class HivemindServer {
|
|
|
188
188
|
required: [],
|
|
189
189
|
},
|
|
190
190
|
},
|
|
191
|
+
{
|
|
192
|
+
name: 'query_asset',
|
|
193
|
+
description: 'Get a specific asset by ID with its generation settings and metadata',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
id: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'Asset ID to query',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
required: ['id'],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'list_assets',
|
|
207
|
+
description: 'List assets with optional filters by entity, type, status, or workflow',
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
depicts: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
description: 'Filter by entity ID depicted in asset',
|
|
214
|
+
},
|
|
215
|
+
assetType: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
enum: ['image', 'audio', 'video', 'document'],
|
|
218
|
+
description: 'Filter by asset type',
|
|
219
|
+
},
|
|
220
|
+
status: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
enum: ['draft', 'pending', 'canon', 'non-canon', 'archived'],
|
|
223
|
+
description: 'Filter by approval status',
|
|
224
|
+
},
|
|
225
|
+
workflowId: {
|
|
226
|
+
type: 'string',
|
|
227
|
+
description: 'Filter by workflow used',
|
|
228
|
+
},
|
|
229
|
+
limit: {
|
|
230
|
+
type: 'number',
|
|
231
|
+
description: 'Maximum results to return (1-100, default 20)',
|
|
232
|
+
default: 20,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
required: [],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'get_canon_status',
|
|
240
|
+
description: 'Get entities grouped by their canon status (draft, pending, canon, non-canon, archived)',
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
status: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
enum: ['draft', 'pending', 'canon', 'non-canon', 'archived'],
|
|
247
|
+
description: 'Filter to specific status',
|
|
248
|
+
},
|
|
249
|
+
type: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
enum: ['character', 'location', 'event', 'faction', 'lore', 'asset'],
|
|
252
|
+
description: 'Filter by entity type',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
required: [],
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'submit_for_review',
|
|
260
|
+
description: 'Submit an entity for review, changing its status from draft to pending',
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
id: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
description: 'Entity ID to submit for review',
|
|
267
|
+
},
|
|
268
|
+
notes: {
|
|
269
|
+
type: 'string',
|
|
270
|
+
description: 'Optional notes for reviewers',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
required: ['id'],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'validate_consistency',
|
|
278
|
+
description: 'Check for consistency issues in the vault (duplicate names, broken links, conflicting data)',
|
|
279
|
+
inputSchema: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
properties: {
|
|
282
|
+
id: {
|
|
283
|
+
type: 'string',
|
|
284
|
+
description: 'Specific entity ID to validate, or omit for full vault check',
|
|
285
|
+
},
|
|
286
|
+
type: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
enum: ['character', 'location', 'event', 'faction', 'lore', 'asset'],
|
|
289
|
+
description: 'Filter validation to specific type',
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
required: [],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
191
295
|
...(this.config.comfyui?.enabled ? [
|
|
192
296
|
{
|
|
193
297
|
name: 'store_workflow',
|
|
@@ -372,6 +476,26 @@ export class HivemindServer {
|
|
|
372
476
|
const parsed = StoreAssetArgsSchema.parse(args);
|
|
373
477
|
return await this.handleStoreAsset(parsed);
|
|
374
478
|
}
|
|
479
|
+
case 'query_asset': {
|
|
480
|
+
const parsed = QueryAssetArgsSchema.parse(args);
|
|
481
|
+
return await this.handleQueryAsset(parsed);
|
|
482
|
+
}
|
|
483
|
+
case 'list_assets': {
|
|
484
|
+
const parsed = ListAssetsArgsSchema.parse(args);
|
|
485
|
+
return await this.handleListAssets(parsed);
|
|
486
|
+
}
|
|
487
|
+
case 'get_canon_status': {
|
|
488
|
+
const parsed = GetCanonStatusArgsSchema.parse(args);
|
|
489
|
+
return await this.handleGetCanonStatus(parsed);
|
|
490
|
+
}
|
|
491
|
+
case 'submit_for_review': {
|
|
492
|
+
const parsed = SubmitForReviewArgsSchema.parse(args);
|
|
493
|
+
return await this.handleSubmitForReview(parsed);
|
|
494
|
+
}
|
|
495
|
+
case 'validate_consistency': {
|
|
496
|
+
const parsed = ValidateConsistencyArgsSchema.parse(args);
|
|
497
|
+
return await this.handleValidateConsistency(parsed);
|
|
498
|
+
}
|
|
375
499
|
default:
|
|
376
500
|
throw new Error(`Unknown tool: ${name}`);
|
|
377
501
|
}
|
|
@@ -1034,6 +1158,302 @@ File watcher keeps index updated automatically.
|
|
|
1034
1158
|
}],
|
|
1035
1159
|
};
|
|
1036
1160
|
}
|
|
1161
|
+
async handleQueryAsset(args) {
|
|
1162
|
+
const asset = this.database.db.prepare(`
|
|
1163
|
+
SELECT * FROM assets WHERE id = ?
|
|
1164
|
+
`).get(args.id);
|
|
1165
|
+
if (!asset) {
|
|
1166
|
+
return {
|
|
1167
|
+
content: [{
|
|
1168
|
+
type: 'text',
|
|
1169
|
+
text: `Asset not found: "${args.id}"`,
|
|
1170
|
+
}],
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
const depicts = asset.depicts ? JSON.parse(asset.depicts) : [];
|
|
1174
|
+
const parameters = asset.parameters ? JSON.parse(asset.parameters) : {};
|
|
1175
|
+
let response = `# Asset: ${asset.id}\n\n`;
|
|
1176
|
+
response += `**Type**: ${asset.asset_type} | **Status**: ${asset.status}\n\n`;
|
|
1177
|
+
response += `## File\n\`${asset.file_path}\`\n\n`;
|
|
1178
|
+
if (depicts.length > 0) {
|
|
1179
|
+
response += `## Depicts\n${depicts.map((d) => `- ${d}`).join('\n')}\n\n`;
|
|
1180
|
+
}
|
|
1181
|
+
if (asset.workflow_id) {
|
|
1182
|
+
response += `## Generation\n`;
|
|
1183
|
+
response += `- **Workflow**: ${asset.workflow_id}\n`;
|
|
1184
|
+
if (asset.prompt)
|
|
1185
|
+
response += `- **Prompt**: ${asset.prompt}\n`;
|
|
1186
|
+
if (Object.keys(parameters).length > 0) {
|
|
1187
|
+
response += `- **Parameters**:\n`;
|
|
1188
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
1189
|
+
response += ` - ${key}: ${value}\n`;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
response += `\n`;
|
|
1193
|
+
}
|
|
1194
|
+
response += `## Metadata\n`;
|
|
1195
|
+
response += `- **Created**: ${asset.created}\n`;
|
|
1196
|
+
response += `- **Status**: ${asset.status}\n`;
|
|
1197
|
+
return {
|
|
1198
|
+
content: [{
|
|
1199
|
+
type: 'text',
|
|
1200
|
+
text: response,
|
|
1201
|
+
}],
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
async handleListAssets(args) {
|
|
1205
|
+
let query = 'SELECT * FROM assets WHERE 1=1';
|
|
1206
|
+
const params = [];
|
|
1207
|
+
if (args.depicts) {
|
|
1208
|
+
query += ` AND depicts LIKE ?`;
|
|
1209
|
+
params.push(`%${args.depicts}%`);
|
|
1210
|
+
}
|
|
1211
|
+
if (args.assetType) {
|
|
1212
|
+
query += ` AND asset_type = ?`;
|
|
1213
|
+
params.push(args.assetType);
|
|
1214
|
+
}
|
|
1215
|
+
if (args.status) {
|
|
1216
|
+
query += ` AND status = ?`;
|
|
1217
|
+
params.push(args.status);
|
|
1218
|
+
}
|
|
1219
|
+
if (args.workflowId) {
|
|
1220
|
+
query += ` AND workflow_id = ?`;
|
|
1221
|
+
params.push(args.workflowId);
|
|
1222
|
+
}
|
|
1223
|
+
query += ` ORDER BY created DESC LIMIT ?`;
|
|
1224
|
+
params.push(args.limit || 20);
|
|
1225
|
+
const assets = this.database.db.prepare(query).all(...params);
|
|
1226
|
+
if (assets.length === 0) {
|
|
1227
|
+
return {
|
|
1228
|
+
content: [{
|
|
1229
|
+
type: 'text',
|
|
1230
|
+
text: 'No assets found matching the specified filters.',
|
|
1231
|
+
}],
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
let response = `# Assets\n\nFound ${assets.length} asset(s):\n\n`;
|
|
1235
|
+
for (const asset of assets) {
|
|
1236
|
+
const depicts = asset.depicts ? JSON.parse(asset.depicts) : [];
|
|
1237
|
+
response += `## ${asset.id}\n`;
|
|
1238
|
+
response += `- **Type**: ${asset.asset_type} | **Status**: ${asset.status}\n`;
|
|
1239
|
+
response += `- **Path**: \`${asset.file_path}\`\n`;
|
|
1240
|
+
if (depicts.length > 0) {
|
|
1241
|
+
response += `- **Depicts**: ${depicts.join(', ')}\n`;
|
|
1242
|
+
}
|
|
1243
|
+
if (asset.workflow_id) {
|
|
1244
|
+
response += `- **Workflow**: ${asset.workflow_id}\n`;
|
|
1245
|
+
}
|
|
1246
|
+
response += `- **Created**: ${asset.created}\n`;
|
|
1247
|
+
response += `\n`;
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
content: [{
|
|
1251
|
+
type: 'text',
|
|
1252
|
+
text: response,
|
|
1253
|
+
}],
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
async handleGetCanonStatus(args) {
|
|
1257
|
+
await this.ensureIndexed();
|
|
1258
|
+
const allNotes = this.vaultReader.getAllNotes();
|
|
1259
|
+
// Group notes by status
|
|
1260
|
+
const byStatus = {
|
|
1261
|
+
draft: [],
|
|
1262
|
+
pending: [],
|
|
1263
|
+
canon: [],
|
|
1264
|
+
'non-canon': [],
|
|
1265
|
+
archived: [],
|
|
1266
|
+
};
|
|
1267
|
+
for (const note of allNotes) {
|
|
1268
|
+
const status = note.frontmatter.status || 'draft';
|
|
1269
|
+
const type = note.frontmatter.type;
|
|
1270
|
+
// Apply filters
|
|
1271
|
+
if (args.status && status !== args.status)
|
|
1272
|
+
continue;
|
|
1273
|
+
if (args.type && type !== args.type)
|
|
1274
|
+
continue;
|
|
1275
|
+
if (byStatus[status]) {
|
|
1276
|
+
byStatus[status].push({
|
|
1277
|
+
id: note.id,
|
|
1278
|
+
title: note.frontmatter.title || note.fileName,
|
|
1279
|
+
type: type,
|
|
1280
|
+
path: note.filePath,
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
let response = `# Canon Status Overview\n\n`;
|
|
1285
|
+
const statusLabels = {
|
|
1286
|
+
draft: 'Draft (Work in Progress)',
|
|
1287
|
+
pending: 'Pending Review',
|
|
1288
|
+
canon: 'Canon (Approved)',
|
|
1289
|
+
'non-canon': 'Non-Canon',
|
|
1290
|
+
archived: 'Archived',
|
|
1291
|
+
};
|
|
1292
|
+
for (const [status, notes] of Object.entries(byStatus)) {
|
|
1293
|
+
if (args.status && status !== args.status)
|
|
1294
|
+
continue;
|
|
1295
|
+
response += `## ${statusLabels[status]} (${notes.length})\n\n`;
|
|
1296
|
+
if (notes.length === 0) {
|
|
1297
|
+
response += `*No entities*\n\n`;
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
for (const note of notes) {
|
|
1301
|
+
response += `- **${note.title}** (${note.type}) — \`${note.id}\`\n`;
|
|
1302
|
+
}
|
|
1303
|
+
response += `\n`;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
// Summary
|
|
1307
|
+
const total = Object.values(byStatus).reduce((sum, arr) => sum + arr.length, 0);
|
|
1308
|
+
response += `---\n**Total**: ${total} entities\n`;
|
|
1309
|
+
return {
|
|
1310
|
+
content: [{
|
|
1311
|
+
type: 'text',
|
|
1312
|
+
text: response,
|
|
1313
|
+
}],
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
async handleSubmitForReview(args) {
|
|
1317
|
+
await this.ensureIndexed();
|
|
1318
|
+
// Find the note
|
|
1319
|
+
const note = this.vaultReader.getAllNotes().find(n => n.id === args.id);
|
|
1320
|
+
if (!note) {
|
|
1321
|
+
return {
|
|
1322
|
+
content: [{
|
|
1323
|
+
type: 'text',
|
|
1324
|
+
text: `Entity not found: "${args.id}"`,
|
|
1325
|
+
}],
|
|
1326
|
+
isError: true,
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
const currentStatus = note.frontmatter.status || 'draft';
|
|
1330
|
+
if (currentStatus !== 'draft') {
|
|
1331
|
+
return {
|
|
1332
|
+
content: [{
|
|
1333
|
+
type: 'text',
|
|
1334
|
+
text: `Cannot submit for review: Entity "${args.id}" is currently "${currentStatus}", not "draft".\n\nOnly draft entities can be submitted for review.`,
|
|
1335
|
+
}],
|
|
1336
|
+
isError: true,
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
// Note: This tool reports what needs to change but doesn't modify files directly.
|
|
1340
|
+
// The user should update the frontmatter manually or use Obsidian.
|
|
1341
|
+
let response = `# Submit for Review: ${note.frontmatter.title || note.fileName}\n\n`;
|
|
1342
|
+
response += `**Entity**: ${args.id}\n`;
|
|
1343
|
+
response += `**Current Status**: ${currentStatus}\n`;
|
|
1344
|
+
response += `**New Status**: pending\n\n`;
|
|
1345
|
+
if (args.notes) {
|
|
1346
|
+
response += `**Review Notes**: ${args.notes}\n\n`;
|
|
1347
|
+
}
|
|
1348
|
+
response += `## Action Required\n\n`;
|
|
1349
|
+
response += `Update the frontmatter in \`${note.filePath}\`:\n\n`;
|
|
1350
|
+
response += `\`\`\`yaml\n`;
|
|
1351
|
+
response += `status: pending\n`;
|
|
1352
|
+
response += `\`\`\`\n\n`;
|
|
1353
|
+
response += `The MCP server will detect the change automatically.\n`;
|
|
1354
|
+
return {
|
|
1355
|
+
content: [{
|
|
1356
|
+
type: 'text',
|
|
1357
|
+
text: response,
|
|
1358
|
+
}],
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
async handleValidateConsistency(args) {
|
|
1362
|
+
await this.ensureIndexed();
|
|
1363
|
+
const allNotes = this.vaultReader.getAllNotes();
|
|
1364
|
+
const issues = [];
|
|
1365
|
+
// Filter notes if specific ID or type requested
|
|
1366
|
+
let notesToCheck = allNotes;
|
|
1367
|
+
if (args.id) {
|
|
1368
|
+
notesToCheck = allNotes.filter(n => n.id === args.id);
|
|
1369
|
+
if (notesToCheck.length === 0) {
|
|
1370
|
+
return {
|
|
1371
|
+
content: [{
|
|
1372
|
+
type: 'text',
|
|
1373
|
+
text: `Entity not found: "${args.id}"`,
|
|
1374
|
+
}],
|
|
1375
|
+
isError: true,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (args.type) {
|
|
1380
|
+
notesToCheck = notesToCheck.filter(n => n.frontmatter.type === args.type);
|
|
1381
|
+
}
|
|
1382
|
+
// Check 1: Duplicate IDs
|
|
1383
|
+
const idCounts = new Map();
|
|
1384
|
+
for (const note of allNotes) {
|
|
1385
|
+
const paths = idCounts.get(note.id) || [];
|
|
1386
|
+
paths.push(note.filePath);
|
|
1387
|
+
idCounts.set(note.id, paths);
|
|
1388
|
+
}
|
|
1389
|
+
for (const [id, paths] of idCounts) {
|
|
1390
|
+
if (paths.length > 1) {
|
|
1391
|
+
issues.push(`**Duplicate ID** \`${id}\` found in:\n${paths.map(p => ` - ${p}`).join('\n')}`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
// Check 2: Broken wikilinks
|
|
1395
|
+
for (const note of notesToCheck) {
|
|
1396
|
+
for (const link of note.links) {
|
|
1397
|
+
const linkLower = link.toLowerCase();
|
|
1398
|
+
// Check if link resolves to any note
|
|
1399
|
+
const found = allNotes.some(n => n.id === link ||
|
|
1400
|
+
(n.frontmatter.title || n.fileName).toLowerCase() === linkLower ||
|
|
1401
|
+
n.fileName.toLowerCase() === linkLower ||
|
|
1402
|
+
n.fileName.toLowerCase() === linkLower + '.md');
|
|
1403
|
+
if (!found) {
|
|
1404
|
+
issues.push(`**Broken link** in \`${note.filePath}\`: [[${link}]]`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
// Check 3: Missing required fields
|
|
1409
|
+
for (const note of notesToCheck) {
|
|
1410
|
+
if (!note.frontmatter.type) {
|
|
1411
|
+
issues.push(`**Missing type** in \`${note.filePath}\``);
|
|
1412
|
+
}
|
|
1413
|
+
if (!note.frontmatter.id && !note.id) {
|
|
1414
|
+
issues.push(`**Missing ID** in \`${note.filePath}\``);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
// Check 4: Canon entities referencing non-canon
|
|
1418
|
+
const canonNotes = allNotes.filter(n => n.frontmatter.status === 'canon');
|
|
1419
|
+
for (const note of canonNotes) {
|
|
1420
|
+
if (!notesToCheck.includes(note) && !args.id)
|
|
1421
|
+
continue;
|
|
1422
|
+
for (const link of note.links) {
|
|
1423
|
+
const linkedNote = allNotes.find(n => n.id === link ||
|
|
1424
|
+
(n.frontmatter.title || n.fileName).toLowerCase() === link.toLowerCase());
|
|
1425
|
+
if (linkedNote && linkedNote.frontmatter.status === 'draft') {
|
|
1426
|
+
issues.push(`**Canon → Draft reference**: \`${note.filePath}\` links to draft entity [[${link}]]`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
// Format response
|
|
1431
|
+
let response = `# Consistency Validation\n\n`;
|
|
1432
|
+
if (args.id) {
|
|
1433
|
+
response += `**Checking**: ${args.id}\n\n`;
|
|
1434
|
+
}
|
|
1435
|
+
else if (args.type) {
|
|
1436
|
+
response += `**Checking**: All ${args.type} entities\n\n`;
|
|
1437
|
+
}
|
|
1438
|
+
else {
|
|
1439
|
+
response += `**Checking**: Full vault (${allNotes.length} entities)\n\n`;
|
|
1440
|
+
}
|
|
1441
|
+
if (issues.length === 0) {
|
|
1442
|
+
response += `✅ **No issues found!**\n\nAll checked entities are consistent.`;
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
response += `⚠️ **Found ${issues.length} issue(s)**:\n\n`;
|
|
1446
|
+
for (const issue of issues) {
|
|
1447
|
+
response += `${issue}\n\n`;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
content: [{
|
|
1452
|
+
type: 'text',
|
|
1453
|
+
text: response,
|
|
1454
|
+
}],
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1037
1457
|
async start() {
|
|
1038
1458
|
// Initial vault scan
|
|
1039
1459
|
console.error('Performing initial vault scan...');
|