Skip to content

Commit fba8acc

Browse files
committed
Bump to 0.4.2: fix biome check (import sorting, delete operator, line length)
1 parent 8e1ae9c commit fba8acc

11 files changed

Lines changed: 120 additions & 12 deletions

File tree

UPJACK_ISSUE_TOOL_VISIBILITY.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Feature: Tool visibility whitelist in manifest
2+
3+
## Problem
4+
5+
`create_server()` registers every auto-generated tool for every entity — CRUD (6), graph traversal (3), and activity tools per entity type. For an app with 6 entities, that's 67+ tools before any custom tools are added.
6+
7+
This overwhelms LLM clients:
8+
- Claude Desktop took 16 seconds to process the `tools/list` response (278K chars of tool schemas)
9+
- LLMs struggle with tool selection when presented with 77 options
10+
- Most auto-generated tools are irrelevant for the use case (e.g., `create_session` and `delete_speaker` on a read-only conference app)
11+
12+
## Current workaround
13+
14+
Monkey-patching `_list_tools` after `create_server()`:
15+
16+
```python
17+
mcp = create_server(str(MANIFEST), root=str(WORKSPACE))
18+
19+
_VISIBLE_TOOLS = {"find_sessions", "create_bookmark", "get_speaker", ...}
20+
21+
_original_list_tools = mcp._list_tools
22+
23+
async def _filtered_list_tools():
24+
all_tools = await _original_list_tools()
25+
return [t for t in all_tools if t.name in _VISIBLE_TOOLS]
26+
27+
mcp._list_tools = _filtered_list_tools
28+
```
29+
30+
This works — hidden tools remain registered and callable via `tools/call`, they just don't appear in `tools/list`. But it's fragile and requires knowing FastMCP internals.
31+
32+
## Proposed solution
33+
34+
### Option A: Manifest-level visibility per entity
35+
36+
```json
37+
{
38+
"entities": [
39+
{
40+
"name": "session",
41+
"prefix": "ss",
42+
"schema": "schemas/session.schema.json",
43+
"tools": {
44+
"visible": ["get", "search"]
45+
}
46+
},
47+
{
48+
"name": "bookmark",
49+
"prefix": "bk",
50+
"schema": "schemas/bookmark.schema.json",
51+
"tools": {
52+
"visible": ["create", "list", "delete"]
53+
}
54+
}
55+
]
56+
}
57+
```
58+
59+
`tools.visible` is a whitelist of which auto-generated tool categories to include in `tools/list`. Options: `create`, `get`, `update`, `list`, `search`, `delete`, `query_by_relationship`, `get_related`, `get_composite`.
60+
61+
Default (no `tools` key): all tools visible (current behavior).
62+
63+
### Option B: `create_server()` parameter
64+
65+
```python
66+
mcp = create_server(
67+
manifest_path,
68+
root="./workspace",
69+
visible_tools=["create_bookmark", "list_bookmarks", "get_session", ...]
70+
)
71+
```
72+
73+
### Option C: Global visibility config in manifest
74+
75+
```json
76+
{
77+
"_meta": {
78+
"ai.nimblebrain/upjack": {
79+
"tool_visibility": {
80+
"mode": "whitelist",
81+
"include": ["create_bookmark", "list_bookmarks", "delete_bookmark", ...]
82+
}
83+
}
84+
}
85+
}
86+
```
87+
88+
## Recommendation
89+
90+
Option A is the best — it's declarative, per-entity, and lives in the manifest where the entity definitions already are. The app author decides at schema time which operations are relevant for each entity type.
91+
92+
For reference data entities (session, speaker, sponsor), you'd set `"visible": ["get"]` or `"visible": ["get", "search"]`. For personal data entities (bookmark, note, connection), you'd set `"visible": ["create", "list", "delete"]` or similar.
93+
94+
## Impact
95+
96+
The MCP Dev Summit app went from 77 tools to 21 visible tools with the workaround. The `tools/list` response dropped from 278K chars to ~50K, and tool selection accuracy improved significantly.
97+
98+
## Notes
99+
100+
- Hidden tools must remain callable — `tools/call` with a hidden tool name should still work
101+
- This is a listing/discoverability concern, not a security concern
102+
- Graph traversal and activity tools should also be controllable (default: hidden unless opted in)
103+
- The `seed_data`, `add_field`, and `rebuild_index` utility tools should have their own visibility toggle

lib/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "upjack"
3-
version = "0.4.1"
3+
version = "0.4.2"
44
description = "Schema-driven entity management for AI-native applications"
55
readme = "README.md"
66
license = {text = "Apache-2.0"}

lib/python/src/upjack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""NimbleBrain Upjack — schema-driven entity management for AI-native applications."""
22

3-
__version__ = "0.4.1"
3+
__version__ = "0.4.2"
44

55
from upjack.activity import ACTIVITY_ENTITY_DEF, get_activity_schema
66
from upjack.app import UpjackApp

lib/python/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/typescript/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "upjack",
3-
"version": "0.4.1",
3+
"version": "0.4.2",
44
"description": "Schema-driven entity management for AI-native applications",
55
"type": "module",
66
"exports": {

lib/typescript/src/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { readFileSync } from "node:fs";
22
import { dirname, join } from "node:path";
33
import { fileURLToPath } from "node:url";
4-
// eslint-disable-next-line @typescript-eslint/no-require-imports -- AJV CJS interop
5-
import Ajv2020Module from "ajv/dist/2020.js";
64
// eslint-disable-next-line @typescript-eslint/no-require-imports -- ajv-formats CJS interop
75
import addFormatsModule from "ajv-formats";
6+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- AJV CJS interop
7+
import Ajv2020Module from "ajv/dist/2020.js";
88

99
// CJS default export interop
1010
const Ajv2020 =

lib/typescript/src/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,9 @@ export function createServer(manifestPath: string, root?: string): Server {
329329
const { definitions, handlers } = buildEntityTools(app, entityDef, schema);
330330
Object.assign(allHandlers, handlers);
331331

332-
const toolsFilter = (entityDef as unknown as Record<string, unknown>).tools as string[] | undefined;
332+
const toolsFilter = (entityDef as unknown as Record<string, unknown>).tools as
333+
| string[]
334+
| undefined;
333335
if (toolsFilter) {
334336
const listed = resolveListedTools(entityDef.name, entityDef.plural, toolsFilter);
335337
listedDefinitions.push(...definitions.filter((d) => listed.has(d.name)));

lib/typescript/tests/app.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs";
1+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2+
import { existsSync } from "node:fs";
23
import { tmpdir } from "node:os";
34
import { join } from "node:path";
45
import { beforeEach, describe, expect, it } from "vitest";
56
import { UpjackApp } from "../src/app.js";
67
import { validateId } from "../src/ids.js";
7-
import { existsSync } from "node:fs";
88

99
const NAMESPACE = "apps/crm";
1010
const ENTITIES = [

lib/typescript/tests/paths.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mkdirSync } from "node:fs";
2-
import { resolve } from "node:path";
32
import { mkdtempSync } from "node:fs";
43
import { tmpdir } from "node:os";
4+
import { resolve } from "node:path";
55
import { join } from "node:path";
66
import { afterEach, describe, expect, it } from "vitest";
77
import { entityDir, entityPath, resolveRoot, schemaDir } from "../src/paths.js";
@@ -75,6 +75,7 @@ describe("resolveRoot", () => {
7575
if (originalEnv !== undefined) {
7676
process.env.UPJACK_ROOT = originalEnv;
7777
} else {
78+
// biome-ignore lint/performance/noDelete: env var cleanup requires delete
7879
delete process.env.UPJACK_ROOT;
7980
}
8081
});
@@ -85,11 +86,13 @@ describe("resolveRoot", () => {
8586
});
8687

8788
it("uses cliRoot when no env var", () => {
89+
// biome-ignore lint/performance/noDelete: env var cleanup requires delete
8890
delete process.env.UPJACK_ROOT;
8991
expect(resolveRoot("/explicit")).toBe(resolve("/explicit"));
9092
});
9193

9294
it("falls back to .upjack", () => {
95+
// biome-ignore lint/performance/noDelete: env var cleanup requires delete
9396
delete process.env.UPJACK_ROOT;
9497
expect(resolveRoot()).toBe(resolve(".upjack"));
9598
});

skills/upjack-app-builder/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name: upjack-app-builder
33
description: Builds complete, compliant NimbleBrain Upjack apps from natural language descriptions. Generates manifest, entity schemas, skills, context, seed data, and server entry point. Use when creating a new Upjack app, building an AI-native app, or scaffolding a domain-specific application. Triggers include "create me a todo app", "build an upjack app", "new upjack app for", "scaffold an app".
44
metadata:
5-
version: "0.4.1"
5+
version: "0.4.2"
66
category: development
77
tags:
88
- upjack

0 commit comments

Comments
 (0)