maggraph 0.4.1__tar.gz → 0.4.2__tar.gz
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.
- {maggraph-0.4.1 → maggraph-0.4.2}/Cargo.lock +2 -2
- {maggraph-0.4.1 → maggraph-0.4.2}/Cargo.toml +1 -1
- {maggraph-0.4.1 → maggraph-0.4.2}/PKG-INFO +6 -5
- {maggraph-0.4.1 → maggraph-0.4.2}/README.md +5 -4
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/__init__.py +1 -1
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/agent/mod.rs +238 -49
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/index.rs +140 -2
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/lib.rs +5 -3
- {maggraph-0.4.1 → maggraph-0.4.2}/pyproject.toml +1 -1
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/Cargo.toml +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/__init__.pyi +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/benches/index_scale.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/benches/traversal.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/py.typed +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/batch.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/config/mod.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/config/schema.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/error.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/graph.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/lakehouse/cache.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/lakehouse/content.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/lakehouse/mod.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/lakehouse/resolver.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/lakehouse/uri.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/memory.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/node/frontmatter.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/node/mod.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/python/mod.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/query.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/recall.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/security.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/sync/engine.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/sync/lock.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/sync/mod.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/sync/policy.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/sync/repo.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/ui/api.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/ui/assets/app.js +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/ui/assets/index.html +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/ui/assets/style.css +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/ui/mod.rs +0 -0
- {maggraph-0.4.1 → maggraph-0.4.2}/maggraph/src/wikilink.rs +0 -0
|
@@ -839,7 +839,7 @@ checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
|
|
|
839
839
|
|
|
840
840
|
[[package]]
|
|
841
841
|
name = "maggraph"
|
|
842
|
-
version = "0.4.
|
|
842
|
+
version = "0.4.2"
|
|
843
843
|
dependencies = [
|
|
844
844
|
"axum",
|
|
845
845
|
"chrono",
|
|
@@ -862,7 +862,7 @@ dependencies = [
|
|
|
862
862
|
|
|
863
863
|
[[package]]
|
|
864
864
|
name = "maggraph-cli"
|
|
865
|
-
version = "0.4.
|
|
865
|
+
version = "0.4.2"
|
|
866
866
|
dependencies = [
|
|
867
867
|
"assert_cmd",
|
|
868
868
|
"axum",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maggraph
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -173,12 +173,13 @@ remote_sources = [
|
|
|
173
173
|
## MCP server scaffold
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
|
-
maggraph scaffold --mcp --output ./
|
|
176
|
+
maggraph scaffold --mcp --output ./agent
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
Generates a ready-to-run FastMCP server at `./mcp_server/server.py` wired to
|
|
180
|
-
your graph index — expose `list_nodes`, `
|
|
181
|
-
and `delete_node` as MCP tools with one command.
|
|
179
|
+
Generates a ready-to-run FastMCP server at `./agent/mcp_server/server.py` wired to
|
|
180
|
+
your graph index — expose `list_nodes`, `get_node`, `traverse_graph`, `create_node`,
|
|
181
|
+
`update_node`, and `delete_node` as MCP tools with one command. Add `--read-only`
|
|
182
|
+
to leave out the write tools, and `--skill` to write `./agent/maggraph-skill/SKILL.md`.
|
|
182
183
|
|
|
183
184
|
---
|
|
184
185
|
|
|
@@ -136,12 +136,13 @@ remote_sources = [
|
|
|
136
136
|
## MCP server scaffold
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
|
-
maggraph scaffold --mcp --output ./
|
|
139
|
+
maggraph scaffold --mcp --output ./agent
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
Generates a ready-to-run FastMCP server at `./mcp_server/server.py` wired to
|
|
143
|
-
your graph index — expose `list_nodes`, `
|
|
144
|
-
and `delete_node` as MCP tools with one command.
|
|
142
|
+
Generates a ready-to-run FastMCP server at `./agent/mcp_server/server.py` wired to
|
|
143
|
+
your graph index — expose `list_nodes`, `get_node`, `traverse_graph`, `create_node`,
|
|
144
|
+
`update_node`, and `delete_node` as MCP tools with one command. Add `--read-only`
|
|
145
|
+
to leave out the write tools, and `--skill` to write `./agent/maggraph-skill/SKILL.md`.
|
|
145
146
|
|
|
146
147
|
---
|
|
147
148
|
|
|
@@ -203,8 +203,62 @@ pub struct SkillRenderContext<'a> {
|
|
|
203
203
|
pub maggraph_version: &'a str,
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/// Options shared by the `SKILL.md` and MCP scaffold generators.
|
|
207
|
+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
208
|
+
#[non_exhaustive]
|
|
209
|
+
pub struct ScaffoldOptions {
|
|
210
|
+
/// Generate a read-only MCP server: omit the `create_node`, `update_node`, and
|
|
211
|
+
/// `delete_node` tools (and leave them out of the generated docs).
|
|
212
|
+
pub read_only: bool,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
impl ScaffoldOptions {
|
|
216
|
+
/// Options for a read-only scaffold.
|
|
217
|
+
pub fn read_only() -> Self {
|
|
218
|
+
Self { read_only: true }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Default directory name, under the scaffold output directory, that holds `SKILL.md`.
|
|
223
|
+
pub const SKILL_DIR_NAME: &str = "maggraph-skill";
|
|
224
|
+
|
|
225
|
+
/// `requirements.txt` line for the published `maggraph` Python bindings, pinned to
|
|
226
|
+
/// this crate's compatible release range (same minor for 0.x, same major after 1.0).
|
|
227
|
+
pub fn maggraph_python_requirement() -> String {
|
|
228
|
+
let major: u64 = env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap_or(0);
|
|
229
|
+
let minor: u64 = env!("CARGO_PKG_VERSION_MINOR").parse().unwrap_or(0);
|
|
230
|
+
if major == 0 {
|
|
231
|
+
format!(
|
|
232
|
+
"maggraph>={major}.{minor},<{major}.{next}",
|
|
233
|
+
next = minor + 1
|
|
234
|
+
)
|
|
235
|
+
} else {
|
|
236
|
+
format!("maggraph>={major}.{minor},<{next}", next = major + 1)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const READ_TOOLS_TABLE: &str = "| `list_nodes` | All indexed node ids |
|
|
241
|
+
| `get_node` | Node metadata + body as Markdown |
|
|
242
|
+
| `traverse_graph` | BFS/DFS traversal Markdown report |
|
|
243
|
+
";
|
|
244
|
+
|
|
245
|
+
const WRITE_TOOLS_TABLE: &str =
|
|
246
|
+
"| `create_node` | Create a new markdown node (optional write policy) |
|
|
247
|
+
| `update_node` | Update node body/frontmatter |
|
|
248
|
+
| `delete_node` | Remove a node from the index |
|
|
249
|
+
";
|
|
250
|
+
|
|
206
251
|
/// Render `SKILL.md` content from schema and context.
|
|
207
252
|
pub fn render_skill_md(schema: &GraphSchema, ctx: &SkillRenderContext<'_>) -> String {
|
|
253
|
+
render_skill_md_with(schema, ctx, ScaffoldOptions::default())
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Render `SKILL.md` content from schema, context, and scaffold options.
|
|
257
|
+
pub fn render_skill_md_with(
|
|
258
|
+
schema: &GraphSchema,
|
|
259
|
+
ctx: &SkillRenderContext<'_>,
|
|
260
|
+
options: ScaffoldOptions,
|
|
261
|
+
) -> String {
|
|
208
262
|
let config_line = ctx
|
|
209
263
|
.config_path
|
|
210
264
|
.map(|p| format!("config: \"{}\"\n", p.display()))
|
|
@@ -340,25 +394,21 @@ Async: `read_node_async`, `traverse_async` (see `planning/PYTHON.md`).
|
|
|
340
394
|
|
|
341
395
|
## MCP server
|
|
342
396
|
|
|
343
|
-
When `mcp_server/` exists
|
|
397
|
+
When `mcp_server/` exists in the scaffold output directory (next to this skill's
|
|
398
|
+
`{skill_dir}/` directory), run from that output directory:
|
|
344
399
|
|
|
345
400
|
```bash
|
|
346
|
-
pip install -r mcp_server/requirements.txt
|
|
347
|
-
# Install maggraph from repo: cd python && maturin develop --release --features python-ext
|
|
401
|
+
pip install -r mcp_server/requirements.txt # fastmcp + maggraph from PyPI
|
|
348
402
|
export MAGGRAPH_CONFIG="{config_display}"
|
|
349
403
|
python mcp_server/server.py
|
|
350
404
|
```
|
|
351
405
|
|
|
406
|
+
{server_mode}
|
|
407
|
+
|
|
352
408
|
| Tool | Description |
|
|
353
409
|
|------|-------------|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
| `traverse_graph` | BFS/DFS traversal Markdown report |
|
|
357
|
-
| `create_node` | Create a new markdown node (optional write policy) |
|
|
358
|
-
| `update_node` | Update node body/frontmatter |
|
|
359
|
-
| `delete_node` | Remove a node from the index |
|
|
360
|
-
|
|
361
|
-
Regenerate when the graph schema changes: `maggraph scaffold --mcp --skill`.
|
|
410
|
+
{tools_table}
|
|
411
|
+
Regenerate when the graph schema changes: `maggraph scaffold --mcp --skill{read_only_flag}`.
|
|
362
412
|
"#,
|
|
363
413
|
version = ctx.maggraph_version,
|
|
364
414
|
root = ctx.graph_root.display(),
|
|
@@ -377,9 +427,29 @@ Regenerate when the graph schema changes: `maggraph scaffold --mcp --skill`.
|
|
|
377
427
|
.config_path
|
|
378
428
|
.map(|p| p.display().to_string())
|
|
379
429
|
.unwrap_or_else(|| "maggraph.toml".to_string()),
|
|
430
|
+
skill_dir = SKILL_DIR_NAME,
|
|
431
|
+
server_mode = if options.read_only {
|
|
432
|
+
"The server is **read-only**: it was generated with `--read-only`, so it exposes no create, update, or delete tools."
|
|
433
|
+
} else {
|
|
434
|
+
"The server is read-write. Regenerate with `--read-only` to omit the create, update, and delete tools."
|
|
435
|
+
},
|
|
436
|
+
tools_table = tools_table(options),
|
|
437
|
+
read_only_flag = if options.read_only {
|
|
438
|
+
" --read-only"
|
|
439
|
+
} else {
|
|
440
|
+
""
|
|
441
|
+
},
|
|
380
442
|
)
|
|
381
443
|
}
|
|
382
444
|
|
|
445
|
+
fn tools_table(options: ScaffoldOptions) -> String {
|
|
446
|
+
if options.read_only {
|
|
447
|
+
READ_TOOLS_TABLE.to_string()
|
|
448
|
+
} else {
|
|
449
|
+
format!("{READ_TOOLS_TABLE}{WRITE_TOOLS_TABLE}")
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
383
453
|
/// Context for MCP scaffold generation.
|
|
384
454
|
#[derive(Debug, Clone)]
|
|
385
455
|
pub struct McpScaffoldContext<'a> {
|
|
@@ -390,6 +460,15 @@ pub struct McpScaffoldContext<'a> {
|
|
|
390
460
|
|
|
391
461
|
/// Write FastMCP server scaffold wired to the `maggraph` Python package.
|
|
392
462
|
pub fn write_mcp_scaffold(dir: &Path, ctx: &McpScaffoldContext<'_>) -> Result<()> {
|
|
463
|
+
write_mcp_scaffold_with(dir, ctx, ScaffoldOptions::default())
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/// Write FastMCP server scaffold with options (for example a read-only server).
|
|
467
|
+
pub fn write_mcp_scaffold_with(
|
|
468
|
+
dir: &Path,
|
|
469
|
+
ctx: &McpScaffoldContext<'_>,
|
|
470
|
+
options: ScaffoldOptions,
|
|
471
|
+
) -> Result<()> {
|
|
393
472
|
fs::create_dir_all(dir).map_err(|e| io_error(format!("create {}: {e}", dir.display())))?;
|
|
394
473
|
|
|
395
474
|
let schema = ctx.schema;
|
|
@@ -441,28 +520,27 @@ Generated by `maggraph scaffold --mcp`. Uses the `maggraph` Python bindings (PyO
|
|
|
441
520
|
## Setup
|
|
442
521
|
|
|
443
522
|
```bash
|
|
444
|
-
#
|
|
445
|
-
cd python && maturin develop --release --features python-ext
|
|
446
|
-
|
|
523
|
+
# Installs fastmcp and the published maggraph bindings from PyPI.
|
|
447
524
|
pip install -r mcp_server/requirements.txt
|
|
448
525
|
export MAGGRAPH_CONFIG="{config}"
|
|
449
526
|
python mcp_server/server.py
|
|
450
527
|
```
|
|
451
528
|
|
|
529
|
+
To run against a MagGraph source checkout instead of the PyPI release, install the
|
|
530
|
+
bindings from the repository's `python/` directory first
|
|
531
|
+
(`pip install maturin && maturin develop --release --features python-ext`) and drop
|
|
532
|
+
the `maggraph` line from `requirements.txt`.
|
|
533
|
+
|
|
452
534
|
## Tools
|
|
453
535
|
|
|
536
|
+
{server_mode}
|
|
537
|
+
|
|
454
538
|
| Tool | Description |
|
|
455
539
|
|------|-------------|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
| `traverse_graph` | BFS/DFS traversal report |
|
|
459
|
-
| `create_node` | Create a new node (id, type, body) |
|
|
460
|
-
| `update_node` | Update node body |
|
|
461
|
-
| `delete_node` | Delete a node by id |
|
|
540
|
+
{tools_table}
|
|
541
|
+
See `planning/MCP.md` in the MagGraph repository for deployment (stdio, Cursor, CI).
|
|
462
542
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
Regenerate when the graph changes: `maggraph scaffold --mcp --skill`.
|
|
543
|
+
Regenerate when the graph changes: `maggraph scaffold --mcp --skill{read_only_flag}`.
|
|
466
544
|
"#,
|
|
467
545
|
config = ctx.config_path.display(),
|
|
468
546
|
root = ctx.graph_root.display(),
|
|
@@ -474,11 +552,28 @@ Regenerate when the graph changes: `maggraph scaffold --mcp --skill`.
|
|
|
474
552
|
} else {
|
|
475
553
|
ids_sample
|
|
476
554
|
},
|
|
555
|
+
server_mode = if options.read_only {
|
|
556
|
+
"Read-only server (generated with `--read-only`): no create, update, or delete tools."
|
|
557
|
+
} else {
|
|
558
|
+
"Read-write server. Regenerate with `--read-only` to omit the create, update, and delete tools."
|
|
559
|
+
},
|
|
560
|
+
tools_table = tools_table(options),
|
|
561
|
+
read_only_flag = if options.read_only {
|
|
562
|
+
" --read-only"
|
|
563
|
+
} else {
|
|
564
|
+
""
|
|
565
|
+
},
|
|
477
566
|
);
|
|
478
567
|
|
|
479
|
-
let requirements =
|
|
480
|
-
|
|
481
|
-
|
|
568
|
+
let requirements = format!(
|
|
569
|
+
"fastmcp>=2.0
|
|
570
|
+
{maggraph_req}
|
|
571
|
+
# maggraph is the MagGraph Python bindings package on PyPI (https://pypi.org/project/maggraph/).
|
|
572
|
+
# To use a source checkout instead, remove the line above and run, from the MagGraph
|
|
573
|
+
# repository's python/ directory: maturin develop --release --features python-ext
|
|
574
|
+
",
|
|
575
|
+
maggraph_req = maggraph_python_requirement(),
|
|
576
|
+
);
|
|
482
577
|
|
|
483
578
|
let server_py = format!(
|
|
484
579
|
r##"# MagGraph FastMCP server - wired to maggraph Python bindings.
|
|
@@ -497,6 +592,8 @@ CONFIG_PATH = os.environ.get("MAGGRAPH_CONFIG", r"{config}")
|
|
|
497
592
|
GRAPH_ROOT = r"{root}"
|
|
498
593
|
NODE_TYPES = {node_types:?}
|
|
499
594
|
KNOWN_NODE_IDS = {node_ids:?}
|
|
595
|
+
# True when generated with `maggraph scaffold --read-only` (write tools omitted).
|
|
596
|
+
READ_ONLY = {read_only}
|
|
500
597
|
|
|
501
598
|
|
|
502
599
|
@lru_cache(maxsize=1)
|
|
@@ -527,7 +624,35 @@ def traverse_graph(from_id: str, depth: int = 2, order: str = "bfs") -> str:
|
|
|
527
624
|
idx = _index()
|
|
528
625
|
result = idx.traverse(from_id, depth, order)
|
|
529
626
|
return result.to_markdown(idx)
|
|
627
|
+
{write_tools}
|
|
628
|
+
|
|
629
|
+
if __name__ == "__main__":
|
|
630
|
+
mcp.run()
|
|
631
|
+
"##,
|
|
632
|
+
config = ctx.config_path.display(),
|
|
633
|
+
root = ctx.graph_root.display(),
|
|
634
|
+
node_types = schema.node_types,
|
|
635
|
+
node_ids = schema.node_ids,
|
|
636
|
+
read_only = if options.read_only { "True" } else { "False" },
|
|
637
|
+
write_tools = if options.read_only {
|
|
638
|
+
""
|
|
639
|
+
} else {
|
|
640
|
+
WRITE_TOOLS_PY
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
fs::write(dir.join("README.md"), readme)
|
|
645
|
+
.map_err(|e| io_error(format!("write README.md: {e}")))?;
|
|
646
|
+
fs::write(dir.join("requirements.txt"), requirements)
|
|
647
|
+
.map_err(|e| io_error(format!("write requirements.txt: {e}")))?;
|
|
648
|
+
fs::write(dir.join("server.py"), server_py)
|
|
649
|
+
.map_err(|e| io_error(format!("write server.py: {e}")))?;
|
|
650
|
+
|
|
651
|
+
Ok(())
|
|
652
|
+
}
|
|
530
653
|
|
|
654
|
+
/// FastMCP write tools appended to `server.py` unless the scaffold is read-only.
|
|
655
|
+
const WRITE_TOOLS_PY: &str = r#"
|
|
531
656
|
|
|
532
657
|
@mcp.tool
|
|
533
658
|
def create_node(
|
|
@@ -538,41 +663,23 @@ def create_node(
|
|
|
538
663
|
) -> str:
|
|
539
664
|
"""Create a new markdown node. Requires write access (local or sync leader with lock)."""
|
|
540
665
|
_index().create_node(node_id, node_type, body, links)
|
|
541
|
-
return f"created node {
|
|
666
|
+
return f"created node {node_id!r}"
|
|
542
667
|
|
|
543
668
|
|
|
544
669
|
@mcp.tool
|
|
545
670
|
def update_node(node_id: str, body: str) -> str:
|
|
546
671
|
"""Replace a node's Markdown body (frontmatter preserved)."""
|
|
547
672
|
_index().update_node(node_id, body)
|
|
548
|
-
return f"updated node {
|
|
673
|
+
return f"updated node {node_id!r}"
|
|
549
674
|
|
|
550
675
|
|
|
551
676
|
@mcp.tool
|
|
552
677
|
def delete_node(node_id: str) -> str:
|
|
553
678
|
"""Delete a node by id."""
|
|
554
679
|
_index().delete_node(node_id)
|
|
555
|
-
return f"deleted node {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if __name__ == "__main__":
|
|
559
|
-
mcp.run()
|
|
560
|
-
"##,
|
|
561
|
-
config = ctx.config_path.display(),
|
|
562
|
-
root = ctx.graph_root.display(),
|
|
563
|
-
node_types = schema.node_types,
|
|
564
|
-
node_ids = schema.node_ids,
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
fs::write(dir.join("README.md"), readme)
|
|
568
|
-
.map_err(|e| io_error(format!("write README.md: {e}")))?;
|
|
569
|
-
fs::write(dir.join("requirements.txt"), requirements)
|
|
570
|
-
.map_err(|e| io_error(format!("write requirements.txt: {e}")))?;
|
|
571
|
-
fs::write(dir.join("server.py"), server_py)
|
|
572
|
-
.map_err(|e| io_error(format!("write server.py: {e}")))?;
|
|
680
|
+
return f"deleted node {node_id!r}"
|
|
573
681
|
|
|
574
|
-
|
|
575
|
-
}
|
|
682
|
+
"#;
|
|
576
683
|
|
|
577
684
|
/// Write `SKILL.md` to `path`.
|
|
578
685
|
pub fn write_skill_md(
|
|
@@ -580,7 +687,17 @@ pub fn write_skill_md(
|
|
|
580
687
|
schema: &GraphSchema,
|
|
581
688
|
ctx: &SkillRenderContext<'_>,
|
|
582
689
|
) -> Result<()> {
|
|
583
|
-
|
|
690
|
+
write_skill_md_with(path, schema, ctx, ScaffoldOptions::default())
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/// Write `SKILL.md` to `path` using scaffold options.
|
|
694
|
+
pub fn write_skill_md_with(
|
|
695
|
+
path: &Path,
|
|
696
|
+
schema: &GraphSchema,
|
|
697
|
+
ctx: &SkillRenderContext<'_>,
|
|
698
|
+
options: ScaffoldOptions,
|
|
699
|
+
) -> Result<()> {
|
|
700
|
+
let content = render_skill_md_with(schema, ctx, options);
|
|
584
701
|
if let Some(parent) = path.parent() {
|
|
585
702
|
fs::create_dir_all(parent)
|
|
586
703
|
.map_err(|e| io_error(format!("create {}: {e}", parent.display())))?;
|
|
@@ -649,5 +766,77 @@ mod tests {
|
|
|
649
766
|
assert!(server.contains("import maggraph"));
|
|
650
767
|
assert!(server.contains("def traverse_graph"));
|
|
651
768
|
assert!(server.contains("def create_node"));
|
|
769
|
+
assert!(server.contains("def delete_node"));
|
|
770
|
+
assert!(server.contains("READ_ONLY = False"));
|
|
771
|
+
assert!(server.contains("return f\"created node {node_id!r}\""));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
fn basic_schema() -> (PathBuf, GraphSchema) {
|
|
775
|
+
let root =
|
|
776
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../examples/basic/knowledge_graph");
|
|
777
|
+
let index = GraphIndex::open(&root).expect("open");
|
|
778
|
+
let schema = GraphSchema::introspect(&index).expect("introspect");
|
|
779
|
+
(root, schema)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
#[test]
|
|
783
|
+
fn read_only_mcp_scaffold_omits_write_tools() {
|
|
784
|
+
let (root, schema) = basic_schema();
|
|
785
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
786
|
+
let mcp_dir = tmp.path().join("mcp_server");
|
|
787
|
+
let ctx = McpScaffoldContext {
|
|
788
|
+
graph_root: &root,
|
|
789
|
+
config_path: Path::new("maggraph.toml"),
|
|
790
|
+
schema: &schema,
|
|
791
|
+
};
|
|
792
|
+
write_mcp_scaffold_with(&mcp_dir, &ctx, ScaffoldOptions::read_only()).expect("write");
|
|
793
|
+
let server = fs::read_to_string(mcp_dir.join("server.py")).expect("server");
|
|
794
|
+
assert!(server.contains("def traverse_graph"));
|
|
795
|
+
assert!(server.contains("READ_ONLY = True"));
|
|
796
|
+
assert!(!server.contains("create_node"));
|
|
797
|
+
assert!(!server.contains("update_node"));
|
|
798
|
+
assert!(!server.contains("delete_node"));
|
|
799
|
+
let readme = fs::read_to_string(mcp_dir.join("README.md")).expect("readme");
|
|
800
|
+
assert!(!readme.contains("`delete_node`"));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
#[test]
|
|
804
|
+
fn read_only_skill_omits_write_tools() {
|
|
805
|
+
let (root, schema) = basic_schema();
|
|
806
|
+
let ctx = SkillRenderContext {
|
|
807
|
+
graph_root: &root,
|
|
808
|
+
config_path: Some(Path::new("maggraph.toml")),
|
|
809
|
+
storage_mode: Some("local"),
|
|
810
|
+
maggraph_version: "0.4.1",
|
|
811
|
+
};
|
|
812
|
+
let rw = render_skill_md(&schema, &ctx);
|
|
813
|
+
assert!(rw.contains("`create_node`"));
|
|
814
|
+
let ro = render_skill_md_with(&schema, &ctx, ScaffoldOptions::read_only());
|
|
815
|
+
assert!(ro.contains("`traverse_graph`"));
|
|
816
|
+
assert!(!ro.contains("`create_node`"));
|
|
817
|
+
assert!(ro.contains("maggraph scaffold --mcp --skill --read-only"));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
#[test]
|
|
821
|
+
fn requirements_pin_published_bindings_to_compatible_range() {
|
|
822
|
+
let req = maggraph_python_requirement();
|
|
823
|
+
let major = env!("CARGO_PKG_VERSION_MAJOR");
|
|
824
|
+
let minor = env!("CARGO_PKG_VERSION_MINOR");
|
|
825
|
+
assert!(
|
|
826
|
+
req.starts_with(&format!("maggraph>={major}.{minor},<")),
|
|
827
|
+
"{req}"
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
let (root, schema) = basic_schema();
|
|
831
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
832
|
+
let ctx = McpScaffoldContext {
|
|
833
|
+
graph_root: &root,
|
|
834
|
+
config_path: Path::new("maggraph.toml"),
|
|
835
|
+
schema: &schema,
|
|
836
|
+
};
|
|
837
|
+
write_mcp_scaffold(tmp.path(), &ctx).expect("write");
|
|
838
|
+
let requirements = fs::read_to_string(tmp.path().join("requirements.txt")).expect("req");
|
|
839
|
+
assert!(requirements.lines().any(|line| line == req));
|
|
840
|
+
assert!(requirements.contains("pypi.org/project/maggraph"));
|
|
652
841
|
}
|
|
653
842
|
}
|
|
@@ -36,12 +36,39 @@ impl NodeIndexEntry {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/// File names (matched case-insensitively) that are commonly documentation rather
|
|
40
|
+
/// than graph nodes. When one of these files does not parse as a node it is skipped
|
|
41
|
+
/// and reported through [`GraphIndex::skipped_files`] instead of failing the scan.
|
|
42
|
+
/// A file with one of these names that *is* a valid node is still indexed.
|
|
43
|
+
pub const NON_NODE_FILE_NAMES: &[&str] = &["SKILL.md", "README.md", "AGENTS.md", "CLAUDE.md"];
|
|
44
|
+
|
|
45
|
+
/// A markdown file under the graph root that the scan skipped because it is not a node.
|
|
46
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
47
|
+
pub struct SkippedFile {
|
|
48
|
+
/// Path relative to the graph root.
|
|
49
|
+
pub relative_path: PathBuf,
|
|
50
|
+
/// Why the file was not indexed (the node parse error).
|
|
51
|
+
pub reason: String,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Whether `path` has one of the [`NON_NODE_FILE_NAMES`].
|
|
55
|
+
pub fn is_non_node_file_name(path: &Path) -> bool {
|
|
56
|
+
path.file_name()
|
|
57
|
+
.and_then(|name| name.to_str())
|
|
58
|
+
.is_some_and(|name| {
|
|
59
|
+
NON_NODE_FILE_NAMES
|
|
60
|
+
.iter()
|
|
61
|
+
.any(|reserved| reserved.eq_ignore_ascii_case(name))
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
/// In-memory index of markdown nodes under a graph root.
|
|
40
66
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
41
67
|
pub struct GraphIndex {
|
|
42
68
|
root_path: PathBuf,
|
|
43
69
|
by_id: HashMap<String, NodeIndexEntry>,
|
|
44
70
|
by_path: HashMap<PathBuf, String>,
|
|
71
|
+
skipped: Vec<SkippedFile>,
|
|
45
72
|
}
|
|
46
73
|
|
|
47
74
|
impl GraphIndex {
|
|
@@ -71,6 +98,7 @@ impl GraphIndex {
|
|
|
71
98
|
root_path,
|
|
72
99
|
by_id: HashMap::new(),
|
|
73
100
|
by_path: HashMap::new(),
|
|
101
|
+
skipped: Vec::new(),
|
|
74
102
|
};
|
|
75
103
|
index.rescan()?;
|
|
76
104
|
Ok(index)
|
|
@@ -80,6 +108,7 @@ impl GraphIndex {
|
|
|
80
108
|
pub fn rescan(&mut self) -> Result<()> {
|
|
81
109
|
self.by_id.clear();
|
|
82
110
|
self.by_path.clear();
|
|
111
|
+
self.skipped.clear();
|
|
83
112
|
|
|
84
113
|
for entry in WalkDir::new(&self.root_path)
|
|
85
114
|
.follow_links(false)
|
|
@@ -111,13 +140,39 @@ impl GraphIndex {
|
|
|
111
140
|
continue;
|
|
112
141
|
}
|
|
113
142
|
|
|
114
|
-
let node = Node::from_file(path, &self.root_path)
|
|
143
|
+
let node = match Node::from_file(path, &self.root_path) {
|
|
144
|
+
Ok(node) => node,
|
|
145
|
+
Err(MagGraphError::NodeParse { message, .. })
|
|
146
|
+
if is_non_node_file_name(relative_path) =>
|
|
147
|
+
{
|
|
148
|
+
tracing::warn!(
|
|
149
|
+
path = %relative_path.display(),
|
|
150
|
+
reason = %message,
|
|
151
|
+
"skipping non-node markdown file"
|
|
152
|
+
);
|
|
153
|
+
self.skipped.push(SkippedFile {
|
|
154
|
+
relative_path: relative_path.to_path_buf(),
|
|
155
|
+
reason: message,
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
Err(err) => return Err(err),
|
|
160
|
+
};
|
|
115
161
|
self.insert_entry(node.metadata, relative_path.to_path_buf(), node.body)?;
|
|
116
162
|
}
|
|
117
163
|
|
|
164
|
+
self.skipped
|
|
165
|
+
.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
|
|
118
166
|
Ok(())
|
|
119
167
|
}
|
|
120
168
|
|
|
169
|
+
/// Markdown files the last full scan skipped because they are documentation
|
|
170
|
+
/// (see [`NON_NODE_FILE_NAMES`]) that does not parse as a node, such as a
|
|
171
|
+
/// generated `SKILL.md` or a `README.md` without node frontmatter.
|
|
172
|
+
pub fn skipped_files(&self) -> &[SkippedFile] {
|
|
173
|
+
&self.skipped
|
|
174
|
+
}
|
|
175
|
+
|
|
121
176
|
pub fn root_path(&self) -> &Path {
|
|
122
177
|
&self.root_path
|
|
123
178
|
}
|
|
@@ -175,10 +230,28 @@ impl GraphIndex {
|
|
|
175
230
|
if let Some(existing_id) = self.by_path.remove(&relative_path) {
|
|
176
231
|
self.by_id.remove(&existing_id);
|
|
177
232
|
}
|
|
233
|
+
self.skipped.retain(|s| s.relative_path != relative_path);
|
|
178
234
|
return Ok(None);
|
|
179
235
|
}
|
|
180
236
|
|
|
181
|
-
let node = Node::from_file(&full_path, &self.root_path)
|
|
237
|
+
let node = match Node::from_file(&full_path, &self.root_path) {
|
|
238
|
+
Ok(node) => node,
|
|
239
|
+
Err(MagGraphError::NodeParse { message, .. })
|
|
240
|
+
if is_non_node_file_name(&relative_path) =>
|
|
241
|
+
{
|
|
242
|
+
if let Some(existing_id) = self.by_path.remove(&relative_path) {
|
|
243
|
+
self.by_id.remove(&existing_id);
|
|
244
|
+
}
|
|
245
|
+
self.skipped.retain(|s| s.relative_path != relative_path);
|
|
246
|
+
self.skipped.push(SkippedFile {
|
|
247
|
+
relative_path,
|
|
248
|
+
reason: message,
|
|
249
|
+
});
|
|
250
|
+
return Ok(None);
|
|
251
|
+
}
|
|
252
|
+
Err(err) => return Err(err),
|
|
253
|
+
};
|
|
254
|
+
self.skipped.retain(|s| s.relative_path != relative_path);
|
|
182
255
|
let id = node.id().to_string();
|
|
183
256
|
if let Some(existing) = self.by_id.get(&id) {
|
|
184
257
|
if existing.relative_path != relative_path {
|
|
@@ -615,6 +688,71 @@ links: ["welcome"]
|
|
|
615
688
|
assert!(!root.join("customer_churn_q2.md").exists());
|
|
616
689
|
}
|
|
617
690
|
|
|
691
|
+
#[test]
|
|
692
|
+
fn scan_skips_and_reports_documentation_files_that_are_not_nodes() {
|
|
693
|
+
let temp = TempDir::new().expect("temp dir");
|
|
694
|
+
let root = temp.path().join("graph");
|
|
695
|
+
write_example_graph(&root);
|
|
696
|
+
// A generated SKILL.md has frontmatter but no `id`; a README has none at all.
|
|
697
|
+
fs::write(
|
|
698
|
+
root.join("SKILL.md"),
|
|
699
|
+
"---\nmaggraph_skill_version: \"0.4.1\"\nnode_count: 2\n---\n\n# Skill\n",
|
|
700
|
+
)
|
|
701
|
+
.expect("write skill");
|
|
702
|
+
fs::create_dir_all(root.join("docs")).expect("docs dir");
|
|
703
|
+
fs::write(root.join("docs/readme.md"), "# Notes about this graph\n").expect("readme");
|
|
704
|
+
|
|
705
|
+
let mut index = GraphIndex::open(&root).expect("open index despite non-node files");
|
|
706
|
+
assert_eq!(index.len(), 2);
|
|
707
|
+
let skipped: Vec<_> = index
|
|
708
|
+
.skipped_files()
|
|
709
|
+
.iter()
|
|
710
|
+
.map(|s| s.relative_path.clone())
|
|
711
|
+
.collect();
|
|
712
|
+
assert_eq!(
|
|
713
|
+
skipped,
|
|
714
|
+
vec![PathBuf::from("SKILL.md"), PathBuf::from("docs/readme.md")]
|
|
715
|
+
);
|
|
716
|
+
assert!(index.skipped_files()[0].reason.contains("id"));
|
|
717
|
+
|
|
718
|
+
// Incremental refresh of a skipped file is a no-op rather than an error.
|
|
719
|
+
assert_eq!(index.update_file("SKILL.md").expect("refresh skill"), None);
|
|
720
|
+
assert_eq!(index.skipped_files().len(), 2);
|
|
721
|
+
fs::remove_file(root.join("SKILL.md")).expect("remove skill");
|
|
722
|
+
assert_eq!(
|
|
723
|
+
index.update_file("SKILL.md").expect("refresh removed"),
|
|
724
|
+
None
|
|
725
|
+
);
|
|
726
|
+
assert_eq!(index.skipped_files().len(), 1);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
#[test]
|
|
730
|
+
fn readme_that_is_a_valid_node_is_still_indexed() {
|
|
731
|
+
let temp = TempDir::new().expect("temp dir");
|
|
732
|
+
let root = temp.path().join("graph");
|
|
733
|
+
write_example_graph(&root);
|
|
734
|
+
fs::write(
|
|
735
|
+
root.join("README.md"),
|
|
736
|
+
"---\nid: graph_readme\ntype: note\n---\n\n# Readme node\n",
|
|
737
|
+
)
|
|
738
|
+
.expect("write readme");
|
|
739
|
+
|
|
740
|
+
let index = GraphIndex::open(&root).expect("open index");
|
|
741
|
+
assert!(index.contains("graph_readme"));
|
|
742
|
+
assert!(index.skipped_files().is_empty());
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
#[test]
|
|
746
|
+
fn ordinary_malformed_node_still_fails_the_scan() {
|
|
747
|
+
let temp = TempDir::new().expect("temp dir");
|
|
748
|
+
let root = temp.path().join("graph");
|
|
749
|
+
write_example_graph(&root);
|
|
750
|
+
fs::write(root.join("broken.md"), "---\ntype: note\n---\n\nNo id\n").expect("write");
|
|
751
|
+
|
|
752
|
+
let err = GraphIndex::open(&root).expect_err("malformed node must fail");
|
|
753
|
+
assert!(matches!(err, MagGraphError::NodeParse { .. }));
|
|
754
|
+
}
|
|
755
|
+
|
|
618
756
|
#[test]
|
|
619
757
|
fn detects_duplicate_ids_on_scan() {
|
|
620
758
|
let temp = TempDir::new().expect("temp dir");
|
|
@@ -18,8 +18,10 @@ pub mod ui;
|
|
|
18
18
|
pub mod wikilink;
|
|
19
19
|
|
|
20
20
|
pub use agent::{
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
maggraph_python_requirement, render_skill_md, render_skill_md_with, write_mcp_scaffold,
|
|
22
|
+
write_mcp_scaffold_with, write_skill_md, write_skill_md_with, EdgePattern, EdgeSource,
|
|
23
|
+
GraphSchema, McpScaffoldContext, ScaffoldOptions, SchemaEdge, SkillRenderContext,
|
|
24
|
+
SKILL_DIR_NAME,
|
|
23
25
|
};
|
|
24
26
|
pub use batch::{
|
|
25
27
|
apply_memory_batch, validate_memory_batch, MemoryBatchOperation, MemoryBatchResult,
|
|
@@ -32,7 +34,7 @@ pub use error::{MagGraphError, Result};
|
|
|
32
34
|
pub use graph::{
|
|
33
35
|
resolve_target, traverse, GraphAdjacency, TraversalNode, TraversalOrder, TraversalResult,
|
|
34
36
|
};
|
|
35
|
-
pub use index::{GraphIndex, NodeIndexEntry};
|
|
37
|
+
pub use index::{GraphIndex, NodeIndexEntry, SkippedFile};
|
|
36
38
|
pub use lakehouse::{
|
|
37
39
|
ContentCache, ContentResolver, FileResolver, HttpResolver, LakehouseReader, NodeWithContent,
|
|
38
40
|
ResolvedContent, ResolverRegistry, S3StubResolver,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|