diff --git a/resources/scripts/napseer_mcp_server.py b/resources/scripts/napseer_mcp_server.py
index e422e6b..b72ca08 100755
--- a/resources/scripts/napseer_mcp_server.py
+++ b/resources/scripts/napseer_mcp_server.py
@@ -10041,7 +10041,7 @@ def ui_tree_insert(root, node):
cursor.setdefault("nodes", []).append(node)
-def ui_render_tree_branch(branch, selected_path, mode, search, prefix=""):
+def ui_render_tree_branch(branch, selected_path, selected_node_id, mode, search, prefix=""):
folders = branch.get("folders", {})
nodes = branch.get("nodes", [])
output = []
@@ -10052,14 +10052,15 @@ def ui_render_tree_branch(branch, selected_path, mode, search, prefix=""):
output.append(
f""
f"dir{html.escape(name)}
"
- f"{ui_render_tree_branch(child, selected_path, mode, search, folder_path)}"
+ f"{ui_render_tree_branch(child, selected_path, selected_node_id, mode, search, folder_path)}"
" "
)
for node in sort_nodes_by_recency(nodes):
full_path = node.get("full_path") or ""
+ node_id = node.get("id") or ""
name = full_path.rstrip("/").split("/")[-1] or full_path
- params = urllib.parse.urlencode({"mode": mode, "q": search, "path": full_path})
- active = " active" if full_path == selected_path else ""
+ params = urllib.parse.urlencode({"mode": mode, "q": search, "node_id": node_id})
+ active = " active" if node_id and node_id == selected_node_id else ""
output.append(
f"node"
f"{html.escape(name)}{html.escape(node.get('type') or 'note')}"
@@ -10241,6 +10242,7 @@ def start_local_ui(args):
"feedback": [],
}
search = query.get("q", [""])[0]
+ selected_node_id = query.get("node_id", [""])[0]
selected_path = query.get("path", [""])[0]
nodes_query = {"limit": 200}
if search:
@@ -10257,14 +10259,21 @@ def start_local_ui(args):
agent_nodes = [node for node in all_nodes if is_agent_namespace_path(node.get("full_path") or "")]
if is_agent_namespace_path(selected_path):
selected_path = ""
- if not selected_path and nodes:
- selected_path = nodes[0].get("full_path") or ""
- selected_node = None
- if selected_path:
+ if not selected_node_id and selected_path:
try:
- selected_node = read_node_by_path({"path": selected_path})
+ selected_node_id = (node_by_path({"path": selected_path}).get("identity") or {}).get("node_id") or ""
+ except Exception:
+ selected_node_id = ""
+ if not selected_node_id and nodes:
+ selected_node_id = nodes[0].get("id") or ""
+ selected_node = None
+ if selected_node_id:
+ try:
+ selected_node = get_node_by_id({"node_id": selected_node_id})
+ selected_path = selected_node.get("full_path") or ""
except Exception:
selected_node = None
+ selected_path = ""
schedules = request_json("GET", f"/v1/projects/{project_id}/schedules").get("items", [])
try:
feedback = request_json("GET", "/v1/feedback?limit=50").get("items", [])
@@ -10320,6 +10329,7 @@ def start_local_ui(args):
"archived_nodes": archived_nodes,
"agent_nodes": agent_nodes,
"connected_agents": connected_agents,
+ "selected_node_id": selected_node_id,
"selected_path": selected_path,
"selected_node": selected_node,
"schedules": schedules,
@@ -10453,6 +10463,7 @@ def start_local_ui(args):
mode = query.get("mode", ["filesystem"])[0]
if mode not in {"filesystem", "path-list"}:
mode = "filesystem"
+ selected_node_id = query.get("node_id", [""])[0]
selected_path = query.get("path", [""])[0]
nodes_query = {"limit": 200}
if search:
@@ -10460,32 +10471,40 @@ def start_local_ui(args):
nodes_suffix = rest_query_string(nodes_query)
nodes = request_json("GET", f"/v1/projects/{project_id}/nodes?{nodes_suffix}").get("items", [])
nodes = sort_nodes_by_recency(nodes)
- if not selected_path and nodes:
- selected_path = nodes[0].get("full_path") or ""
- selected_node = None
- if selected_path:
+ if not selected_node_id and selected_path:
try:
- selected_node = read_node_by_path({"path": selected_path})
+ selected_node_id = (node_by_path({"path": selected_path}).get("identity") or {}).get("node_id") or ""
+ except Exception:
+ selected_node_id = ""
+ if not selected_node_id and nodes:
+ selected_node_id = nodes[0].get("id") or ""
+ selected_node = None
+ if selected_node_id:
+ try:
+ selected_node = get_node_by_id({"node_id": selected_node_id})
+ selected_path = selected_node.get("full_path") or ""
except Exception:
selected_node = None
+ selected_path = ""
schedules = request_json("GET", f"/v1/projects/{project_id}/schedules").get("items", [])
feedback = request_json("GET", "/v1/feedback?limit=50").get("items", [])
status = update_status()
current_url = "/" + (f"?{parsed_path.query}" if parsed_path.query else "")
- filesystem_url = "/?" + urllib.parse.urlencode({"mode": "filesystem", "q": search, "path": selected_path})
- path_list_url = "/?" + urllib.parse.urlencode({"mode": "path-list", "q": search, "path": selected_path})
+ filesystem_url = "/?" + urllib.parse.urlencode({"mode": "filesystem", "q": search, "node_id": selected_node_id})
+ path_list_url = "/?" + urllib.parse.urlencode({"mode": "path-list", "q": search, "node_id": selected_node_id})
tree = ui_build_tree(nodes)
- tree_html = ui_render_tree_branch(tree, selected_path, mode, search)
+ tree_html = ui_render_tree_branch(tree, selected_path, selected_node_id, mode, search)
selected_links = ui_json_list((selected_node or {}).get("links") or [])
selected_metadata = ui_json((selected_node or {}).get("metadata") or {})
selected_tags = ", ".join((selected_node or {}).get("tags") or [])
selected_content = html.escape((selected_node or {}).get("content_text") or "")
selected_type = html.escape((selected_node or {}).get("type") or "note")
selected_path_value = html.escape(selected_path or "")
+ selected_node_id_value = html.escape(selected_node_id or "")
selected_title = html.escape(selected_path or "New node")
selected_badges = ui_tags((selected_node or {}).get("tags") or [])
node_rows = "".join(
- f"
{html.escape(item['full_path'])} | {ui_tags(item.get('tags') or [])} | {html.escape(item.get('type') or 'note')} | |
"
+ f"{html.escape(item['full_path'])} | {ui_tags(item.get('tags') or [])} | {html.escape(item.get('type') or 'note')} | |
"
for item in nodes
)
schedule_rows = "".join(
@@ -10516,13 +10535,13 @@ def start_local_ui(args):
{f'''
{selected_title}
{selected_badges}
''' if mode == 'filesystem' else f'''
| Path | Tags | Type | Action |
{node_rows or '| No nodes. |
'}
'''}
@@ -10600,20 +10619,24 @@ def start_local_ui(args):
"payload": json.loads((form.get("payload", ["{}"])[0] or "{}")),
})
elif self.path == "/nodes/upsert":
- upsert_node({
+ node_id = form.get("node_id", [""])[0].strip()
+ payload = {
"path": form.get("path", [""])[0],
"type": form.get("type", ["note"])[0],
"tags": [tag.strip() for tag in form.get("tags", [""])[0].split(",") if tag.strip()],
"links": json.loads(form.get("links", ["[]"])[0] or "[]"),
"metadata": json.loads(form.get("metadata", ["{}"])[0] or "{}"),
"content_text": form.get("content_text", [""])[0],
- })
+ }
+ if node_id:
+ payload["node_id"] = node_id
+ upsert_node(payload)
elif self.path == "/nodes/archive":
- archive_node_by_path({"path": form.get("path", [""])[0]})
+ archive_node_by_path({"node_id": form.get("node_id", [""])[0]})
elif self.path == "/nodes/link":
link_nodes({
- "source_path": form.get("source_path", [""])[0],
- "target_path": form.get("target_path", [""])[0],
+ "source_node_id": form.get("source_node_id", [""])[0],
+ "target_node_id": form.get("target_node_id", [""])[0],
"relation": form.get("relation", ["references"])[0],
})
elif self.path == "/auth/renew":
diff --git a/tests/test_mcp_memory_ergonomics_helpers.py b/tests/test_mcp_memory_ergonomics_helpers.py
index 8f5d633..573e628 100644
--- a/tests/test_mcp_memory_ergonomics_helpers.py
+++ b/tests/test_mcp_memory_ergonomics_helpers.py
@@ -632,6 +632,18 @@ def test_context_and_related_use_identity_inputs(mod):
mod.find_related_nodes({"path": "/notes/root"})
+def test_local_helper_ui_forms_submit_identity_for_existing_node_actions():
+ source = (pathlib.Path(__file__).resolve().parents[1] / "resources" / "scripts" / "napseer_mcp_server.py").read_text()
+ assert "name='node_id' value='{html.escape(item.get('id') or '')}'" in source
+ assert 'name="source_node_id"' in source
+ assert 'name="target_node_id"' in source
+ assert 'archive_node_by_path({"node_id": form.get("node_id", [""])[0]})' in source
+ assert '"source_node_id": form.get("source_node_id", [""])[0]' in source
+ assert '"target_node_id": form.get("target_node_id", [""])[0]' in source
+ assert 'name="source_path"' not in source
+ assert 'name="target_path"' not in source
+
+
def test_discovery_rejects_full_content_view(mod):
with pytest.raises(RuntimeError, match="full node bodies require nap_node_get"):
mod.list_project_nodes({"view": "full"})