feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system: - Prisma schema: add gated, priority, summary, chapters, linkTarget fields - Session gate: state machine (gated → begin_session → ungated) with LLM-powered tool selection based on prompt index - Tag matcher: intelligent prompt-to-tool matching with project/server/action tags - LLM selector: tiered provider selection (fast for gating, heavy for complex tasks) - Link resolver: cross-project MCP resource references (project/server:uri format) - Prompt summary service: LLM-generated summaries and chapter extraction - System project bootstrap: ensures default project exists on startup - Structural link health checks: enrichWithLinkStatus on prompt GET endpoints - CLI: create prompt --priority/--link, create project --gated/--no-gated, describe project shows prompts section, get prompts shows PRI/LINK/STATUS - Apply/edit: priority, linkTarget, gated fields supported - Shell completions: fish updated with new flags - 1,253 tests passing across all packages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
392
.taskmaster/docs/prd-gated-prompts.md
Normal file
392
.taskmaster/docs/prd-gated-prompts.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# PRD: Gated Project Experience & Prompt Intelligence
|
||||
|
||||
## Overview
|
||||
|
||||
When 300 developers connect their LLM clients (Claude Code, Cursor, etc.) to mcpctl projects, they need relevant context — security policies, architecture decisions, operational runbooks — without flooding the context window. This feature introduces a gated session flow where the client LLM drives its own context retrieval through keyword-based matching, with the proxy providing a prompt index and encouraging ongoing discovery.
|
||||
|
||||
## Problem
|
||||
|
||||
- Injecting all prompts into instructions doesn't scale (hundreds of pages of policies)
|
||||
- Exposing prompts only as MCP resources means LLMs never read them
|
||||
- An index-only approach works for small numbers but breaks down at scale
|
||||
- No mechanism to link external knowledge (Notion, Docmost) as prompts
|
||||
- LLMs tend to work with whatever they have rather than proactively seek more context
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Gated Experience
|
||||
|
||||
A project-level flag (`gated: boolean`, default: `true`) that controls whether sessions go through a keyword-driven prompt retrieval flow before accessing project tools and resources.
|
||||
|
||||
**Flow (A + C):**
|
||||
|
||||
1. On `initialize`, instructions include the **prompt index** (names + summaries for all prompts, up to a reasonable cap) and tell client LLM: "Call `begin_session` with 5 keywords describing your task"
|
||||
2. **If client obeys**: `begin_session({ tags: ["zigbee", "lights", "mqtt", "pairing", "automation"] })` → prompt selection (see below) → returns matched prompt content + full prompt index + encouragement to retrieve more → session ungated
|
||||
3. **If client ignores**: First `tools/call` is intercepted → keywords extracted from tool name + arguments → same prompt selection → briefing injected alongside tool result → session ungated
|
||||
4. **Ongoing retrieval**: Client can call `read_prompts({ tags: ["security", "vpn"] })` at any point to retrieve more prompts. The prompt index is always visible so the client LLM can see what's available.
|
||||
|
||||
**Prompt selection — tiered approach:**
|
||||
|
||||
- **Primary (heavy LLM available)**: Tags + full prompt index (names, priorities, summaries, chapters) are sent to the heavy LLM (e.g. Gemini). The LLM understands synonyms, context, and intent — it knows "zigbee" relates to "Z2M" and "Zigbee2MQTT", and that someone working on "lights" probably needs the "common-mistakes" prompt about pairing. The LLM returns a ranked list of relevant prompt names with brief explanations of why each is relevant. The heavy LLM may use the fast LLM for preprocessing if needed (e.g. generating missing summaries on the fly).
|
||||
- **Fallback (no LLM, or `llmProvider=none`)**: Deterministic keyword-based tag matching against summaries/chapters with byte-budget allocation (see "Tag Matching Algorithm" below). Same approach as ResponsePaginator's byte-based fallback. Triggered when: no LLM providers configured, project has `llmProvider: "none"`, or local override sets `provider: "none"`.
|
||||
- **Hybrid (both paths always available)**: Even when heavy LLM does the initial selection, the `read_prompts({ tags: [...] })` tool always uses keyword matching. This way the client LLM can retrieve specific prompts by keyword that the heavy LLM may have missed. The LLM is smart about context, keywords are precise about names — together they cover both fuzzy and exact retrieval.
|
||||
|
||||
**LLM availability resolution** (same chain as existing LLM features):
|
||||
- Project `llmProvider: "none"` → no LLM, keyword fallback only
|
||||
- Project `llmProvider: null` → inherit from global config
|
||||
- Local override `provider: "none"` → no LLM, keyword fallback only
|
||||
- No providers configured → keyword fallback only
|
||||
- Otherwise → use heavy LLM for `begin_session`, fast LLM for summary generation
|
||||
|
||||
### Encouraging Retrieval
|
||||
|
||||
LLMs tend to proceed with incomplete information rather than seek more context. The system must actively counter this at multiple points:
|
||||
|
||||
**In `initialize` instructions:**
|
||||
```
|
||||
You have access to project knowledge containing policies, architecture decisions,
|
||||
and guidelines. Some may contain critical rules about what you're doing. After your
|
||||
initial briefing, if you're unsure about conventions, security requirements, or
|
||||
best practices — request more context using read_prompts. It's always better to
|
||||
check than to guess wrong. The project may have specific rules you don't know about yet.
|
||||
```
|
||||
|
||||
**In `begin_session` response (after matched prompts):**
|
||||
```
|
||||
Other prompts available that may become relevant as your work progresses:
|
||||
- security-policies: Network segmentation, firewall rules, VPN access
|
||||
- naming-conventions: Service and resource naming standards
|
||||
- ...
|
||||
If any of these seem related to what you're doing now or later, request them
|
||||
with read_prompts({ tags: [...] }) or resources/read. Don't assume you have
|
||||
all the context — check when in doubt.
|
||||
```
|
||||
|
||||
**In `read_prompts` response:**
|
||||
```
|
||||
Remember: you can request more prompts at any time with read_prompts({ tags: [...] }).
|
||||
The project may have additional guidelines relevant to your current approach.
|
||||
```
|
||||
|
||||
The tone is not "here's optional reading" but "there are rules you might not know about, and violating them costs more than reading them."
|
||||
|
||||
### Prompt Priority (1-10)
|
||||
|
||||
Every prompt has a priority level that influences selection order and byte-budget allocation:
|
||||
|
||||
| Range | Meaning | Behavior |
|
||||
|-------|---------|----------|
|
||||
| 1-3 | Reference | Low priority, included only on strong keyword match |
|
||||
| 4-6 | Standard | Default priority, included on moderate keyword match |
|
||||
| 7-9 | Important | High priority, lower match threshold |
|
||||
| 10 | Critical | Always included in full, regardless of keyword match (guardrails, common mistakes) |
|
||||
|
||||
Default priority for new prompts: `5`.
|
||||
|
||||
### Prompt Summaries & Chapters (Auto-generated)
|
||||
|
||||
Each prompt gets auto-generated metadata used for the prompt index and tag matching:
|
||||
|
||||
- `summary` (string, ~20 words) — one-line description of what the prompt covers
|
||||
- `chapters` (string[]) — key sections/topics extracted from content
|
||||
|
||||
Generation pipeline:
|
||||
- **Fast LLM available**: Summarize content, extract key topics
|
||||
- **No fast LLM**: First sentence of content + markdown headings via regex
|
||||
- Regenerated on prompt create/update
|
||||
- Cached on the prompt record
|
||||
|
||||
### Tag Matching Algorithm (No-LLM Fallback)
|
||||
|
||||
When no local LLM is available, the system falls back to a deterministic retrieval algorithm:
|
||||
|
||||
1. Client provides tags (5 keywords from `begin_session`, or extracted from tool call)
|
||||
2. For each prompt, compute a match score:
|
||||
- Check tags against prompt `summary` and `chapters` (case-insensitive substring match)
|
||||
- Score = `number_of_matching_tags * base_priority`
|
||||
- Priority 10 prompts: score = infinity (always included)
|
||||
3. Sort by score descending
|
||||
4. Fill a byte budget (configurable, default ~8KB) from top down:
|
||||
- Include full content until budget exhausted
|
||||
- Remaining matched prompts: include as index entries (name + summary)
|
||||
- Non-matched prompts: listed as names only in the "other prompts available" section
|
||||
|
||||
**When `begin_session` is skipped (intercept path):**
|
||||
- Extract keywords from tool name + arguments (e.g., `home-assistant/get_entities({ domain: "light" })` → tags: `["home-assistant", "entities", "light"]`)
|
||||
- Run same matching algorithm
|
||||
- Inject briefing alongside the real tool result
|
||||
|
||||
### `read_prompts` Tool (Ongoing Retrieval)
|
||||
|
||||
Available after session is ungated. Allows the client LLM to request more context at any point:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "read_prompts",
|
||||
"description": "Request additional project context by keywords. Use this whenever you need guidelines, policies, or conventions related to your current work. It's better to check than to guess.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Keywords describing what context you need (e.g. [\"security\", \"vpn\", \"firewall\"])"
|
||||
}
|
||||
},
|
||||
"required": ["tags"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Returns matched prompt content + the prompt index reminder.
|
||||
|
||||
### Prompt Links
|
||||
|
||||
A prompt can be a **link** to an MCP resource in another project's server. The linked content is fetched server-side (by the proxy, not the client), enforcing RBAC.
|
||||
|
||||
Format: `project/server:resource-uri`
|
||||
Example: `system-public/docmost-mcp:docmost://pages/architecture-overview`
|
||||
|
||||
Properties:
|
||||
- The proxy fetches linked content using the source project's service account
|
||||
- Client LLM never gets direct access to the source MCP server
|
||||
- Dead links are detected and marked (health check on link resolution)
|
||||
- Dead links generate error log entries
|
||||
|
||||
RBAC for links:
|
||||
- Creating a link requires `edit` permission on RBAC in the target project
|
||||
- A service account permission is created on the source project for the linked resource
|
||||
- Default: admin group members can manage links
|
||||
|
||||
## Schema Changes
|
||||
|
||||
### Project
|
||||
|
||||
Add field:
|
||||
- `gated: boolean` (default: `true`)
|
||||
|
||||
### Prompt
|
||||
|
||||
Add fields:
|
||||
- `priority: integer` (1-10, default: 5)
|
||||
- `summary: string | null` (auto-generated)
|
||||
- `chapters: string[] | null` (auto-generated, stored as JSON)
|
||||
- `linkTarget: string | null` (format: `project/server:resource-uri`, null for regular prompts)
|
||||
|
||||
### PromptRequest
|
||||
|
||||
Add field:
|
||||
- `priority: integer` (1-10, default: 5)
|
||||
|
||||
## API Changes
|
||||
|
||||
### Modified Endpoints
|
||||
|
||||
- `POST /api/v1/prompts` — accept `priority`, `linkTarget`
|
||||
- `PUT /api/v1/prompts/:id` — accept `priority` (not `linkTarget` — links are immutable, delete and recreate)
|
||||
- `POST /api/v1/promptrequests` — accept `priority`
|
||||
- `GET /api/v1/prompts` — return `priority`, `summary`, `linkTarget`, `linkStatus` (alive/dead/unknown)
|
||||
- `GET /api/v1/projects/:name/prompts/visible` — return `priority`, `summary`, `chapters`
|
||||
|
||||
### New Endpoints
|
||||
|
||||
- `POST /api/v1/prompts/:id/regenerate-summary` — force re-generation of summary/chapters
|
||||
- `GET /api/v1/projects/:name/prompt-index` — returns compact index (name, priority, summary, chapters)
|
||||
|
||||
## MCP Protocol Changes (mcplocal router)
|
||||
|
||||
### Session State
|
||||
|
||||
Router tracks per-session state:
|
||||
- `gated: boolean` — starts `true` if project is gated
|
||||
- `tags: string[]` — accumulated tags from begin_session + read_prompts calls
|
||||
- `retrievedPrompts: Set<string>` — prompts already sent to client (avoid re-sending)
|
||||
|
||||
### Gated Session Flow
|
||||
|
||||
1. On `initialize`: instructions include prompt index + gate message + retrieval encouragement
|
||||
2. `tools/list` while gated: only `begin_session` visible (progressive tool exposure)
|
||||
3. `begin_session({ tags })`: match tags → return briefing + prompt index + encouragement → ungate → send `notifications/tools/list_changed`
|
||||
4. On first `tools/call` while still gated: extract keywords → match → inject briefing alongside result → ungate
|
||||
5. After ungating: all tools work normally, `read_prompts` available for ongoing retrieval
|
||||
|
||||
### `begin_session` Tool
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "begin_session",
|
||||
"description": "Start your session by providing 5 keywords that describe your current task. You'll receive relevant project context, policies, and guidelines. Required before using other tools.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"maxItems": 10,
|
||||
"description": "5 keywords describing your current task (e.g. [\"zigbee\", \"automation\", \"lights\", \"mqtt\", \"pairing\"])"
|
||||
}
|
||||
},
|
||||
"required": ["tags"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response structure:
|
||||
```
|
||||
[Priority 10 prompts — always, full content]
|
||||
|
||||
[Tag-matched prompts — full content, byte-budget-capped, priority-ordered]
|
||||
|
||||
Other prompts available that may become relevant as your work progresses:
|
||||
- <name>: <summary>
|
||||
- <name>: <summary>
|
||||
- ...
|
||||
If any of these seem related to what you're doing, request them with
|
||||
read_prompts({ tags: [...] }). Don't assume you have all the context — check.
|
||||
```
|
||||
|
||||
### Prompt Index in Instructions
|
||||
|
||||
The `initialize` instructions include a compact prompt index so the client LLM can see what knowledge exists. Format per prompt: `- <name>: <summary>` (~100 chars max per entry).
|
||||
|
||||
Cap: if more than 50 prompts, include only priority 7+ in instructions index. Full index always available via `resources/list`.
|
||||
|
||||
## CLI Changes
|
||||
|
||||
### New/Modified Commands
|
||||
|
||||
- `mcpctl create prompt <name> --priority <1-10>` — create with priority
|
||||
- `mcpctl create prompt <name> --link <project/server:uri>` — create linked prompt
|
||||
- `mcpctl get prompt -A` — show all prompts across all projects, with link targets
|
||||
- `mcpctl describe project <name>` — show gated status, session greeting, prompt table
|
||||
- `mcpctl edit project <name>` — `gated` field editable
|
||||
|
||||
### Prompt Link Display
|
||||
|
||||
```
|
||||
$ mcpctl get prompt -A
|
||||
PROJECT NAME PRIORITY LINK STATUS
|
||||
homeautomation security-policies 8 - -
|
||||
homeautomation architecture-adr 6 system-public/docmost-mcp:docmost://pages/a1 alive
|
||||
homeautomation common-mistakes 10 - -
|
||||
system-public onboarding 4 - -
|
||||
```
|
||||
|
||||
## Describe Project Output
|
||||
|
||||
```
|
||||
$ mcpctl describe project homeautomation
|
||||
Name: homeautomation
|
||||
Gated: true
|
||||
LLM Provider: gemini-cli
|
||||
...
|
||||
|
||||
Session greeting:
|
||||
You have access to project knowledge containing policies, architecture decisions,
|
||||
and guidelines. Call begin_session with 5 keywords describing your task to receive
|
||||
relevant context. Some prompts contain critical rules — it's better to check than guess.
|
||||
|
||||
Prompts:
|
||||
NAME PRIORITY TYPE LINK
|
||||
common-mistakes 10 local -
|
||||
security-policies 8 local -
|
||||
architecture-adr 6 link system-public/docmost-mcp:docmost://pages/a1
|
||||
stack 5 local -
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
**Full test coverage is required.** Every new module, service, route, and algorithm must have comprehensive tests. No feature ships without tests.
|
||||
|
||||
### Unit Tests (mcpd)
|
||||
- Prompt priority CRUD: create/update/get with priority field, default value, validation (1-10 range)
|
||||
- Prompt link CRUD: create with linkTarget, immutability (can't update linkTarget), delete
|
||||
- Prompt summary generation: auto-generation on create/update, regex fallback when no LLM
|
||||
- `GET /api/v1/prompts` with priority, linkTarget, linkStatus fields
|
||||
- `GET /api/v1/projects/:name/prompt-index` returns compact index
|
||||
- `POST /api/v1/prompts/:id/regenerate-summary` triggers re-generation
|
||||
- Project `gated` field: CRUD, default value
|
||||
|
||||
### Unit Tests (mcplocal — gating flow)
|
||||
- State machine: gated → `begin_session` → ungated (happy path)
|
||||
- State machine: gated → `tools/call` intercepted → ungated (fallback path)
|
||||
- State machine: non-gated project skips gate entirely
|
||||
- LLM selection path: tags + prompt index sent to heavy LLM, ranked results returned, priority 10 always included
|
||||
- LLM selection path: heavy LLM uses fast LLM for missing summary generation
|
||||
- No-LLM fallback: tag matching score calculation, priority weighting, substring matching
|
||||
- No-LLM fallback: byte-budget exhaustion, priority ordering, index fallback, edge cases
|
||||
- Keyword extraction from tool calls: tool name parsing, argument extraction
|
||||
- `begin_session` response: matched content + index + encouragement text (both LLM and fallback paths)
|
||||
- `read_prompts` response: additional matches, deduplication against already-sent prompts (both paths)
|
||||
- Tools blocked while gated: return error directing to `begin_session`
|
||||
- `tools/list` while gated: only `begin_session` visible
|
||||
- `tools/list` after ungating: `begin_session` replaced by `read_prompts` + all upstream tools
|
||||
- Priority 10 always included regardless of tag match or budget
|
||||
- Prompt index in instructions: cap at 50, priority 7+ when over cap
|
||||
- Notifications: `tools/list_changed` sent after ungating
|
||||
|
||||
### Unit Tests (mcplocal — prompt links)
|
||||
- Link resolution: fetch content from source project's MCP server via service account
|
||||
- Dead link detection: source server unavailable, resource not found, permission denied
|
||||
- Dead link marking: status field updated, error logged
|
||||
- RBAC enforcement: link creation requires edit permission on target project RBAC
|
||||
- Service account permission: auto-created on source project for linked resource
|
||||
- Content isolation: client LLM cannot access source server directly
|
||||
|
||||
### Unit Tests (CLI)
|
||||
- `create prompt` with `--priority` flag, validation
|
||||
- `create prompt` with `--link` flag, format validation
|
||||
- `get prompt -A` output: all projects, link targets, status columns
|
||||
- `describe project` output: gated status, session greeting, prompt table
|
||||
- `edit project` with gated field
|
||||
- Shell completions for new flags and resources
|
||||
|
||||
### Integration Tests
|
||||
- End-to-end gated session: connect → begin_session with tags → tools available → correct prompts returned
|
||||
- End-to-end intercept: connect → skip begin_session → call tool → keywords extracted → briefing injected
|
||||
- End-to-end read_prompts: after ungating → request more context → additional prompts returned → no duplicates
|
||||
- Prompt link resolution: create link → fetch content → verify content matches source
|
||||
- Dead link lifecycle: create link → kill source → verify dead detection → restore → verify recovery
|
||||
- Priority ordering: create prompts at various priorities → verify selection order and budget allocation
|
||||
- Encouragement text: verify retrieval encouragement present in begin_session, read_prompts, and instructions
|
||||
|
||||
## System Prompts (mcpctl-system project)
|
||||
|
||||
All gate messages, encouragement text, and briefing templates are stored as prompts in a special `mcpctl-system` project. This makes them editable at runtime via `mcpctl edit prompt` without code changes or redeployment.
|
||||
|
||||
### Required System Prompts
|
||||
|
||||
| Name | Priority | Purpose |
|
||||
|------|----------|---------|
|
||||
| `gate-instructions` | 10 | Text injected into `initialize` instructions for gated projects. Tells client to call `begin_session` with 5 keywords. |
|
||||
| `gate-encouragement` | 10 | Appended after `begin_session` response. Lists remaining prompts and encourages further retrieval. |
|
||||
| `read-prompts-reminder` | 10 | Appended after `read_prompts` response. Reminds client that more context is available. |
|
||||
| `gate-intercept-preamble` | 10 | Prepended to briefing when injected via tool call intercept (Option C fallback). |
|
||||
| `session-greeting` | 10 | Shown in `mcpctl describe project` as the "hello prompt" — what client LLMs see on connect. |
|
||||
|
||||
### Bootstrap
|
||||
|
||||
The `mcpctl-system` project and its system prompts are created automatically on first startup (seed migration). They can be edited afterward but not deleted — delete attempts return an error.
|
||||
|
||||
### How mcplocal Uses Them
|
||||
|
||||
On router initialization, mcplocal fetches system prompts from mcpd via:
|
||||
```
|
||||
GET /api/v1/projects/mcpctl-system/prompts/visible
|
||||
```
|
||||
|
||||
These are cached with the same 60s TTL as project routers. The prompt content supports template variables:
|
||||
- `{{prompt_index}}` — replaced with the current project's prompt index
|
||||
- `{{project_name}}` — replaced with the current project name
|
||||
- `{{matched_prompts}}` — replaced with tag-matched prompt content
|
||||
- `{{remaining_prompts}}` — replaced with the list of non-matched prompts
|
||||
|
||||
This way the encouragement text, tone, and structure can be tuned by editing prompts — no code changes needed.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Prompt links: content fetched server-side, client never gets direct access to source MCP server
|
||||
- RBAC: link creation requires edit permission on target project's RBAC
|
||||
- Service account: source project grants read access to linked resource only
|
||||
- Dead links: logged as errors, marked in listings, never expose source server errors to client
|
||||
- Tag extraction: sanitize tool call arguments before using as keywords (prevent injection)
|
||||
@@ -1408,13 +1408,497 @@
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-21T18:52:29.084Z"
|
||||
},
|
||||
{
|
||||
"id": "37",
|
||||
"title": "Add priority, summary, chapters, and linkTarget fields to Prompt schema",
|
||||
"description": "Extend the Prisma schema for the Prompt model to include priority (integer 1-10, default 5), summary (nullable string), chapters (nullable JSON array), and linkTarget (nullable string for prompt links).",
|
||||
"details": "1. Update `/src/db/prisma/schema.prisma` to add fields to the Prompt model:\n - `priority Int @default(5)` with check constraint 1-10\n - `summary String? @db.Text`\n - `chapters Json?` (stored as JSON array of strings)\n - `linkTarget String?` (format: `project/server:resource-uri`)\n\n2. Create Prisma migration:\n ```bash\n pnpm --filter db exec prisma migrate dev --name add-prompt-priority-summary-chapters-link\n ```\n\n3. Update TypeScript types in shared package to reflect new fields\n\n4. Add validation for priority range (1-10) at the database level if possible, otherwise enforce in application layer",
|
||||
"testStrategy": "- Unit test: Verify migration creates columns with correct types and defaults\n- Unit test: Verify priority default is 5\n- Unit test: Verify nullable fields accept null\n- Unit test: Verify chapters stores/retrieves JSON arrays correctly\n- Integration test: Create prompt with all new fields, retrieve and verify values",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:35:08.154Z"
|
||||
},
|
||||
{
|
||||
"id": "38",
|
||||
"title": "Add priority field to PromptRequest schema",
|
||||
"description": "Extend the Prisma schema for the PromptRequest model to include the priority field (integer 1-10, default 5) to match the Prompt model.",
|
||||
"details": "1. Update `/src/db/prisma/schema.prisma` to add to PromptRequest:\n - `priority Int @default(5)`\n\n2. Create Prisma migration:\n ```bash\n pnpm --filter db exec prisma migrate dev --name add-promptrequest-priority\n ```\n\n3. Update the `CreatePromptRequestSchema` in `/src/mcpd/src/validation/prompt.schema.ts` to include priority validation:\n ```typescript\n priority: z.number().int().min(1).max(10).default(5).optional(),\n ```\n\n4. Update TypeScript types in shared package",
|
||||
"testStrategy": "- Unit test: Migration creates priority column with default 5\n- Unit test: PromptRequest creation with explicit priority\n- Unit test: PromptRequest creation uses default priority when not specified\n- Unit test: Validation rejects priority outside 1-10 range",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"37"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:35:08.160Z"
|
||||
},
|
||||
{
|
||||
"id": "39",
|
||||
"title": "Add gated field to Project schema",
|
||||
"description": "Extend the Prisma schema for the Project model to include the gated boolean field (default true) that controls whether sessions go through the keyword-driven prompt retrieval flow.",
|
||||
"details": "1. Update `/src/db/prisma/schema.prisma` to add to Project:\n - `gated Boolean @default(true)`\n\n2. Create Prisma migration:\n ```bash\n pnpm --filter db exec prisma migrate dev --name add-project-gated\n ```\n\n3. Update project-related TypeScript types\n\n4. Update project validation schemas to include gated field:\n ```typescript\n gated: z.boolean().default(true).optional(),\n ```\n\n5. Update project API routes to accept and return the gated field",
|
||||
"testStrategy": "- Unit test: Migration creates gated column with default true\n- Unit test: Project creation with gated=false\n- Unit test: Project creation uses default gated=true when not specified\n- Unit test: Project update can toggle gated field\n- Integration test: GET /api/v1/projects/:name returns gated field",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:35:08.165Z"
|
||||
},
|
||||
{
|
||||
"id": "40",
|
||||
"title": "Update Prompt CRUD API to handle priority and linkTarget",
|
||||
"description": "Modify prompt API endpoints to accept, validate, and return the priority and linkTarget fields. LinkTarget should be immutable after creation.",
|
||||
"details": "1. Update `/src/mcpd/src/validation/prompt.schema.ts`:\n ```typescript\n export const CreatePromptSchema = z.object({\n name: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),\n content: z.string().min(1).max(50000),\n projectId: z.string().optional(),\n priority: z.number().int().min(1).max(10).default(5).optional(),\n linkTarget: z.string().regex(/^[a-z0-9-]+\\/[a-z0-9-]+:[\\S]+$/).optional(),\n });\n \n export const UpdatePromptSchema = z.object({\n content: z.string().min(1).max(50000).optional(),\n priority: z.number().int().min(1).max(10).optional(),\n // Note: linkTarget is NOT included - links are immutable\n });\n ```\n\n2. Update `/src/mcpd/src/routes/prompts.ts`:\n - POST /api/v1/prompts: Accept priority, linkTarget\n - PUT /api/v1/prompts/:id: Accept priority only (not linkTarget)\n - GET endpoints: Return priority, linkTarget in response\n\n3. Update repository layer to handle new fields\n\n4. Add linkTarget format validation: `project/server:resource-uri`",
|
||||
"testStrategy": "- Unit test: POST /api/v1/prompts with priority creates prompt with correct priority\n- Unit test: POST /api/v1/prompts with linkTarget creates linked prompt\n- Unit test: PUT /api/v1/prompts/:id with priority updates priority\n- Unit test: PUT /api/v1/prompts/:id rejects linkTarget (immutable)\n- Unit test: GET /api/v1/prompts returns priority and linkTarget fields\n- Unit test: Invalid linkTarget format rejected (validation error)\n- Unit test: Priority outside 1-10 range rejected",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"37"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:37:17.506Z"
|
||||
},
|
||||
{
|
||||
"id": "41",
|
||||
"title": "Update PromptRequest API to handle priority",
|
||||
"description": "Modify prompt request API endpoints to accept, validate, and return the priority field for proposed prompts.",
|
||||
"details": "1. Update validation in `/src/mcpd/src/validation/prompt.schema.ts`:\n ```typescript\n export const CreatePromptRequestSchema = z.object({\n name: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),\n content: z.string().min(1).max(50000),\n projectId: z.string().optional(),\n createdBySession: z.string().optional(),\n createdByUserId: z.string().optional(),\n priority: z.number().int().min(1).max(10).default(5).optional(),\n });\n ```\n\n2. Update `/src/mcpd/src/routes/prompts.ts` for PromptRequest endpoints:\n - POST /api/v1/promptrequests: Accept priority\n - GET /api/v1/promptrequests: Return priority\n - POST /api/v1/promptrequests/:id/approve: Preserve priority when creating Prompt\n\n3. Update PromptService.approve() to copy priority from request to prompt\n\n4. Update repository layer",
|
||||
"testStrategy": "- Unit test: POST /api/v1/promptrequests with priority creates request with correct priority\n- Unit test: POST /api/v1/promptrequests uses default priority 5 when not specified\n- Unit test: GET /api/v1/promptrequests returns priority field\n- Unit test: Approve preserves priority from request to created prompt\n- Unit test: Priority validation (1-10 range)",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"38"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:37:17.511Z"
|
||||
},
|
||||
{
|
||||
"id": "42",
|
||||
"title": "Implement prompt summary generation service",
|
||||
"description": "Create a service that auto-generates summary (20 words) and chapters (key sections) for prompts, using fast LLM when available or regex fallback.",
|
||||
"details": "1. Create `/src/mcpd/src/services/prompt-summary.service.ts`:\n ```typescript\n export class PromptSummaryService {\n constructor(\n private llmClient: LlmClient | null,\n private promptRepo: IPromptRepository\n ) {}\n \n async generateSummary(content: string): Promise<{ summary: string; chapters: string[] }> {\n if (this.llmClient) {\n return this.generateWithLlm(content);\n }\n return this.generateWithRegex(content);\n }\n \n private async generateWithLlm(content: string): Promise<...> {\n // Send content to fast LLM with prompt:\n // \"Generate a 20-word summary and extract key section topics...\"\n }\n \n private generateWithRegex(content: string): { summary: string; chapters: string[] } {\n // summary: first sentence of content (truncated to ~20 words)\n // chapters: extract markdown headings via regex /^#+\\s+(.+)$/gm\n }\n }\n ```\n\n2. Integrate with PromptService:\n - Call generateSummary on prompt create\n - Call generateSummary on prompt update (when content changes)\n - Cache results on the prompt record\n\n3. Handle LLM availability check via existing LlmConfig patterns",
|
||||
"testStrategy": "- Unit test: generateWithRegex extracts first sentence as summary\n- Unit test: generateWithRegex extracts markdown headings as chapters\n- Unit test: generateWithLlm calls LLM with correct prompt (mock LLM)\n- Unit test: generateSummary uses LLM when available\n- Unit test: generateSummary falls back to regex when no LLM\n- Unit test: Empty content handled gracefully\n- Unit test: Content without headings returns empty chapters array\n- Integration test: Creating prompt triggers summary generation",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"37"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:39:28.196Z"
|
||||
},
|
||||
{
|
||||
"id": "43",
|
||||
"title": "Add regenerate-summary API endpoint",
|
||||
"description": "Create POST /api/v1/prompts/:id/regenerate-summary endpoint to force re-generation of summary and chapters for a prompt.",
|
||||
"details": "1. Add route in `/src/mcpd/src/routes/prompts.ts`:\n ```typescript\n fastify.post('/api/v1/prompts/:id/regenerate-summary', async (request, reply) => {\n const { id } = request.params as { id: string };\n const prompt = await promptService.findById(id);\n if (!prompt) {\n return reply.status(404).send({ error: 'Prompt not found' });\n }\n \n const { summary, chapters } = await summaryService.generateSummary(prompt.content);\n const updated = await promptService.updateSummary(id, summary, chapters);\n \n return reply.send(updated);\n });\n ```\n\n2. Add `updateSummary(id, summary, chapters)` method to PromptRepository and PromptService\n\n3. Return the updated prompt with new summary/chapters in response",
|
||||
"testStrategy": "- Unit test: POST to valid prompt ID regenerates summary\n- Unit test: Returns updated prompt with new summary/chapters\n- Unit test: 404 for non-existent prompt ID\n- Unit test: Uses LLM when available, regex fallback otherwise\n- Integration test: End-to-end regeneration updates database",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"42"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:39:28.201Z"
|
||||
},
|
||||
{
|
||||
"id": "44",
|
||||
"title": "Create prompt-index API endpoint",
|
||||
"description": "Create GET /api/v1/projects/:name/prompt-index endpoint that returns a compact index of prompts (name, priority, summary, chapters) for a project.",
|
||||
"details": "1. Add route in `/src/mcpd/src/routes/prompts.ts`:\n ```typescript\n fastify.get('/api/v1/projects/:name/prompt-index', async (request, reply) => {\n const { name } = request.params as { name: string };\n const project = await projectService.findByName(name);\n if (!project) {\n return reply.status(404).send({ error: 'Project not found' });\n }\n \n const prompts = await promptService.findByProject(project.id);\n const index = prompts.map(p => ({\n name: p.name,\n priority: p.priority,\n summary: p.summary,\n chapters: p.chapters,\n linkTarget: p.linkTarget,\n }));\n \n return reply.send({ prompts: index });\n });\n ```\n\n2. Consider adding global prompts to the index (inherited by all projects)\n\n3. Sort by priority descending in response",
|
||||
"testStrategy": "- Unit test: Returns compact index for valid project\n- Unit test: Index contains name, priority, summary, chapters, linkTarget\n- Unit test: 404 for non-existent project\n- Unit test: Empty array for project with no prompts\n- Unit test: Results sorted by priority descending\n- Integration test: End-to-end retrieval matches database state",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"42"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:39:28.208Z"
|
||||
},
|
||||
{
|
||||
"id": "45",
|
||||
"title": "Implement tag-matching algorithm for prompt selection",
|
||||
"description": "Create a deterministic keyword-based tag matching algorithm as the no-LLM fallback for prompt selection, with byte-budget allocation and priority weighting.",
|
||||
"details": "1. Create `/src/mcplocal/src/services/tag-matcher.service.ts`:\n ```typescript\n interface MatchedPrompt {\n prompt: PromptIndex;\n score: number;\n matchedTags: string[];\n }\n \n export class TagMatcherService {\n constructor(private byteBudget: number = 8192) {}\n \n matchPrompts(tags: string[], promptIndex: PromptIndex[]): {\n fullContent: PromptIndex[]; // Prompts to include in full\n indexOnly: PromptIndex[]; // Prompts to include as index entries\n remaining: PromptIndex[]; // Non-matched prompts (names only)\n } {\n // 1. Priority 10 prompts: always included (score = Infinity)\n // 2. For each prompt, compute score:\n // - Check tags against summary + chapters (case-insensitive substring)\n // - score = matching_tags_count * priority\n // 3. Sort by score descending\n // 4. Fill byte budget from top:\n // - Include full content until budget exhausted\n // - Remaining matched: include as index entries\n // - Non-matched: names only\n }\n \n private computeScore(tags: string[], prompt: PromptIndex): number {\n if (prompt.priority === 10) return Infinity;\n const matchingTags = tags.filter(tag => \n this.matchesPrompt(tag.toLowerCase(), prompt)\n );\n return matchingTags.length * prompt.priority;\n }\n \n private matchesPrompt(tag: string, prompt: PromptIndex): boolean {\n const searchText = [\n prompt.summary || '',\n ...(prompt.chapters || [])\n ].join(' ').toLowerCase();\n return searchText.includes(tag);\n }\n }\n ```\n\n2. Handle edge cases: empty tags, no prompts, all priority 10, etc.",
|
||||
"testStrategy": "- Unit test: Priority 10 prompts always included regardless of tags\n- Unit test: Score calculation: matching_tags * priority\n- Unit test: Case-insensitive matching\n- Unit test: Substring matching in summary and chapters\n- Unit test: Byte budget exhaustion stops full content inclusion\n- Unit test: Matched prompts beyond budget become index entries\n- Unit test: Non-matched prompts listed as names only\n- Unit test: Sorting by score descending\n- Unit test: Empty tags returns priority 10 only\n- Unit test: No prompts returns empty result",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"44"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:40:47.570Z"
|
||||
},
|
||||
{
|
||||
"id": "46",
|
||||
"title": "Implement LLM-based prompt selection service",
|
||||
"description": "Create a service that uses the heavy LLM to intelligently select relevant prompts based on tags and the full prompt index, understanding synonyms and context.",
|
||||
"details": "1. Create `/src/mcplocal/src/services/llm-prompt-selector.service.ts`:\n ```typescript\n export class LlmPromptSelectorService {\n constructor(\n private llmClient: LlmClient,\n private fastLlmClient: LlmClient | null,\n private tagMatcher: TagMatcherService // fallback\n ) {}\n \n async selectPrompts(tags: string[], promptIndex: PromptIndex[]): Promise<{\n selected: Array<{ name: string; reason: string }>;\n priority10: PromptIndex[]; // Always included\n }> {\n // 1. Extract priority 10 prompts (always included)\n // 2. Generate missing summaries using fast LLM if needed\n // 3. Send to heavy LLM:\n const prompt = `\n Given these keywords: ${tags.join(', ')}\n And this prompt index:\n ${promptIndex.map(p => `- ${p.name}: ${p.summary}`).join('\\n')}\n \n Select the most relevant prompts for someone working on tasks\n related to these keywords. Consider synonyms and related concepts.\n Return a ranked JSON array: [{name: string, reason: string}]\n `;\n // 4. Parse LLM response\n // 5. On LLM error, fall back to tag matcher\n }\n }\n ```\n\n2. Handle LLM timeouts and errors gracefully with fallback\n\n3. Validate LLM response format",
|
||||
"testStrategy": "- Unit test: Priority 10 prompts always returned regardless of LLM selection\n- Unit test: LLM called with correct prompt format (mock)\n- Unit test: LLM response parsed correctly\n- Unit test: Invalid LLM response falls back to tag matcher\n- Unit test: LLM timeout falls back to tag matcher\n- Unit test: Missing summaries trigger fast LLM generation\n- Unit test: No LLM available uses tag matcher directly\n- Integration test: End-to-end selection with mock LLM",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"45"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:45:57.158Z"
|
||||
},
|
||||
{
|
||||
"id": "47",
|
||||
"title": "Implement session state management for gating",
|
||||
"description": "Extend the McpRouter to track per-session gating state including gated status, accumulated tags, and retrieved prompts set.",
|
||||
"details": "1. Update `/src/mcplocal/src/router.ts` to add session state:\n ```typescript\n interface SessionState {\n gated: boolean; // starts true if project is gated\n tags: string[]; // accumulated from begin_session + read_prompts\n retrievedPrompts: Set<string>; // prompts already sent (avoid duplicates)\n }\n \n export class McpRouter {\n private sessionStates: Map<string, SessionState> = new Map();\n \n getSessionState(sessionId: string): SessionState {\n if (!this.sessionStates.has(sessionId)) {\n this.sessionStates.set(sessionId, {\n gated: this.projectConfig?.gated ?? true,\n tags: [],\n retrievedPrompts: new Set(),\n });\n }\n return this.sessionStates.get(sessionId)!;\n }\n \n ungateSession(sessionId: string): void {\n const state = this.getSessionState(sessionId);\n state.gated = false;\n }\n \n addRetrievedPrompts(sessionId: string, names: string[]): void {\n const state = this.getSessionState(sessionId);\n names.forEach(n => state.retrievedPrompts.add(n));\n }\n }\n ```\n\n2. Clean up session state when session closes\n\n3. Handle session state for non-gated projects (gated=false from start)",
|
||||
"testStrategy": "- Unit test: New session starts with gated=true for gated project\n- Unit test: New session starts with gated=false for non-gated project\n- Unit test: ungateSession changes gated to false\n- Unit test: addRetrievedPrompts adds to set\n- Unit test: retrievedPrompts prevents duplicates\n- Unit test: Session state isolated per sessionId\n- Unit test: Session cleanup removes state",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"39"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:45:57.164Z"
|
||||
},
|
||||
{
|
||||
"id": "48",
|
||||
"title": "Implement begin_session tool for gated sessions",
|
||||
"description": "Create the begin_session MCP tool that accepts 5 keywords, triggers prompt selection, returns matched content with encouragement, and ungates the session.",
|
||||
"details": "1. Add begin_session tool definition in `/src/mcplocal/src/router.ts`:\n ```typescript\n private getBeginSessionTool(): Tool {\n return {\n name: 'begin_session',\n description: 'Start your session by providing 5 keywords that describe your current task. You\\'ll receive relevant project context, policies, and guidelines. Required before using other tools.',\n inputSchema: {\n type: 'object',\n properties: {\n tags: {\n type: 'array',\n items: { type: 'string' },\n maxItems: 10,\n description: '5 keywords describing your current task'\n }\n },\n required: ['tags']\n }\n };\n }\n ```\n\n2. Implement begin_session handler:\n - Validate tags array (1-10 items)\n - Call LlmPromptSelector or TagMatcher based on LLM availability\n - Fetch full content for selected prompts\n - Build response with matched content + index + encouragement\n - Ungate session\n - Send `notifications/tools/list_changed`\n\n3. Response format:\n ```\n [Priority 10 prompts - full content]\n \n [Tag-matched prompts - full content, priority-ordered]\n \n Other prompts available that may become relevant...\n - name: summary\n ...\n If any seem related, request them with read_prompts({ tags: [...] }).\n ```",
|
||||
"testStrategy": "- Unit test: begin_session with valid tags returns matched prompts\n- Unit test: begin_session includes priority 10 prompts always\n- Unit test: begin_session response includes encouragement text\n- Unit test: begin_session response includes prompt index\n- Unit test: Session ungated after successful begin_session\n- Unit test: notifications/tools/list_changed sent after ungating\n- Unit test: Empty tags handled (returns priority 10 only)\n- Unit test: Invalid tags rejected with error\n- Unit test: begin_session while already ungated returns error",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"46",
|
||||
"47"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:50:39.111Z"
|
||||
},
|
||||
{
|
||||
"id": "49",
|
||||
"title": "Implement read_prompts tool for ongoing retrieval",
|
||||
"description": "Create the read_prompts MCP tool that allows clients to request additional context by keywords after the session is ungated.",
|
||||
"details": "1. Add read_prompts tool definition:\n ```typescript\n private getReadPromptsTool(): Tool {\n return {\n name: 'read_prompts',\n description: 'Request additional project context by keywords. Use this whenever you need guidelines, policies, or conventions related to your current work.',\n inputSchema: {\n type: 'object',\n properties: {\n tags: {\n type: 'array',\n items: { type: 'string' },\n description: 'Keywords describing what context you need'\n }\n },\n required: ['tags']\n }\n };\n }\n ```\n\n2. Implement read_prompts handler:\n - Always use keyword matching (not LLM) for precision\n - Exclude already-retrieved prompts from response\n - Add newly retrieved prompts to session state\n - Include reminder about more prompts available\n\n3. Response format:\n ```\n [Matched prompt content - deduplicated]\n \n Remember: you can request more prompts at any time with read_prompts({ tags: [...] }).\n The project may have additional guidelines relevant to your current approach.\n ```",
|
||||
"testStrategy": "- Unit test: read_prompts returns matched prompts by keyword\n- Unit test: Already retrieved prompts excluded from response\n- Unit test: Newly retrieved prompts added to session state\n- Unit test: Response includes reminder text\n- Unit test: read_prompts while gated returns error\n- Unit test: Empty tags returns empty response\n- Unit test: Uses keyword matching not LLM",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"48"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:50:39.115Z"
|
||||
},
|
||||
{
|
||||
"id": "50",
|
||||
"title": "Implement progressive tool exposure for gated sessions",
|
||||
"description": "Modify tools/list behavior to only expose begin_session while gated, and expose all tools plus read_prompts after ungating.",
|
||||
"details": "1. Update tools/list handling in `/src/mcplocal/src/router.ts`:\n ```typescript\n async handleToolsList(sessionId: string): Promise<Tool[]> {\n const state = this.getSessionState(sessionId);\n \n if (state.gated) {\n // Only show begin_session while gated\n return [this.getBeginSessionTool()];\n }\n \n // After ungating: all upstream tools + read_prompts\n const upstreamTools = await this.discoverTools();\n return [...upstreamTools, this.getReadPromptsTool()];\n }\n ```\n\n2. Block direct tool calls while gated:\n ```typescript\n async handleToolCall(sessionId: string, toolName: string, args: any): Promise<any> {\n const state = this.getSessionState(sessionId);\n \n if (state.gated && toolName !== 'begin_session') {\n // Intercept: extract keywords, match prompts, inject briefing\n return this.handleInterceptedCall(sessionId, toolName, args);\n }\n \n // Normal routing\n return this.routeToolCall(toolName, args);\n }\n ```\n\n3. Ensure notifications/tools/list_changed is sent after ungating",
|
||||
"testStrategy": "- Unit test: tools/list while gated returns only begin_session\n- Unit test: tools/list after ungating returns all tools + read_prompts\n- Unit test: begin_session not visible after ungating\n- Unit test: Tool call while gated (not begin_session) triggers intercept\n- Unit test: Tool call after ungating routes normally\n- Unit test: notifications/tools/list_changed sent on ungate",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"48",
|
||||
"49"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:50:39.120Z"
|
||||
},
|
||||
{
|
||||
"id": "51",
|
||||
"title": "Implement keyword extraction from tool calls",
|
||||
"description": "Create a service that extracts keywords from tool names and arguments for the intercept fallback path when clients skip begin_session.",
|
||||
"details": "1. Create `/src/mcplocal/src/services/keyword-extractor.service.ts`:\n ```typescript\n export class KeywordExtractorService {\n extractKeywords(toolName: string, args: Record<string, any>): string[] {\n const keywords: string[] = [];\n \n // Extract from tool name (split on / and -)\n // e.g., \"home-assistant/get_entities\" -> [\"home\", \"assistant\", \"get\", \"entities\"]\n keywords.push(...this.extractFromName(toolName));\n \n // Extract from argument values\n // e.g., { domain: \"light\", entity_id: \"light.kitchen\" } -> [\"light\", \"kitchen\"]\n keywords.push(...this.extractFromArgs(args));\n \n // Deduplicate and sanitize\n return [...new Set(keywords.map(k => this.sanitize(k)))];\n }\n \n private sanitize(keyword: string): string {\n // Remove special characters, lowercase, limit length\n return keyword.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 50);\n }\n }\n ```\n\n2. Handle various argument types: strings, arrays, nested objects\n\n3. Prevent injection by sanitizing extracted keywords",
|
||||
"testStrategy": "- Unit test: Extracts keywords from tool name with /\n- Unit test: Extracts keywords from tool name with -\n- Unit test: Extracts keywords from string argument values\n- Unit test: Extracts keywords from array argument values\n- Unit test: Handles nested object arguments\n- Unit test: Sanitizes special characters\n- Unit test: Deduplicates keywords\n- Unit test: Handles empty arguments\n- Unit test: Limits keyword length to prevent abuse",
|
||||
"priority": "medium",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:40:47.575Z"
|
||||
},
|
||||
{
|
||||
"id": "52",
|
||||
"title": "Implement tool call intercept with briefing injection",
|
||||
"description": "When a gated session calls a tool without first calling begin_session, intercept the call, extract keywords, match prompts, and inject the briefing alongside the real tool result.",
|
||||
"details": "1. Implement handleInterceptedCall in `/src/mcplocal/src/router.ts`:\n ```typescript\n async handleInterceptedCall(\n sessionId: string,\n toolName: string,\n args: any\n ): Promise<ToolResult> {\n // 1. Extract keywords from tool call\n const keywords = this.keywordExtractor.extractKeywords(toolName, args);\n \n // 2. Match prompts using keywords\n const { fullContent, indexOnly, remaining } = \n await this.promptSelector.selectPrompts(keywords, this.promptIndex);\n \n // 3. Execute the actual tool call\n const actualResult = await this.routeToolCall(toolName, args);\n \n // 4. Build briefing with intercept preamble\n const briefing = this.buildBriefing(fullContent, indexOnly, remaining, 'intercept');\n \n // 5. Ungate session\n this.ungateSession(sessionId);\n \n // 6. Send notifications/tools/list_changed\n await this.sendToolsListChanged();\n \n // 7. Return combined result\n return {\n content: [{\n type: 'text',\n text: `${briefing}\\n\\n---\\n\\n${actualResult.content[0].text}`\n }]\n };\n }\n ```\n\n2. Use gate-intercept-preamble system prompt for the briefing prefix",
|
||||
"testStrategy": "- Unit test: Tool call while gated triggers intercept\n- Unit test: Keywords extracted from tool name and args\n- Unit test: Prompts matched using extracted keywords\n- Unit test: Actual tool still executes and returns result\n- Unit test: Briefing prepended to tool result\n- Unit test: Session ungated after intercept\n- Unit test: notifications/tools/list_changed sent\n- Unit test: Intercept preamble included in briefing\n- Integration test: End-to-end intercept flow",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"50",
|
||||
"51"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:51:03.822Z"
|
||||
},
|
||||
{
|
||||
"id": "53",
|
||||
"title": "Add prompt index to initialize instructions",
|
||||
"description": "Modify the initialize handler to include the compact prompt index and gate message in instructions for gated projects.",
|
||||
"details": "1. Update initialize handling in `/src/mcplocal/src/router.ts`:\n ```typescript\n async handleInitialize(sessionId: string): Promise<InitializeResult> {\n const state = this.getSessionState(sessionId);\n \n let instructions = this.projectConfig.prompt || '';\n \n if (state.gated) {\n // Add gate instructions\n const gateInstructions = await this.getSystemPrompt('gate-instructions');\n \n // Build prompt index (cap at 50, priority 7+ if over)\n const index = this.buildPromptIndex();\n \n instructions += `\\n\\n${gateInstructions.replace('{{prompt_index}}', index)}`;\n }\n \n return {\n protocolVersion: '2024-11-05',\n capabilities: { ... },\n serverInfo: { ... },\n instructions,\n };\n }\n ```\n\n2. Build prompt index with cap:\n - If <= 50 prompts: include all\n - If > 50 prompts: include only priority 7+\n - Format: `- <name>: <summary>` (~100 chars per entry)",
|
||||
"testStrategy": "- Unit test: Gated project includes gate instructions in initialize\n- Unit test: Prompt index included in instructions\n- Unit test: Index capped at 50 entries\n- Unit test: Over 50 prompts shows priority 7+ only\n- Unit test: Non-gated project skips gate instructions\n- Unit test: {{prompt_index}} template replaced\n- Integration test: End-to-end initialize with gated project",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"47",
|
||||
"44"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:52:13.697Z"
|
||||
},
|
||||
{
|
||||
"id": "54",
|
||||
"title": "Create mcpctl-system project with system prompts",
|
||||
"description": "Implement bootstrap logic to create the mcpctl-system project and its required system prompts on first startup, with protection against deletion.",
|
||||
"details": "1. Create seed migration or startup hook:\n ```typescript\n async function bootstrapSystemProject() {\n const systemProject = await projectRepo.findByName('mcpctl-system');\n if (systemProject) return; // Already exists\n \n // Create mcpctl-system project\n const project = await projectRepo.create({\n name: 'mcpctl-system',\n description: 'System prompts for mcpctl gating and encouragement',\n gated: false, // System project is not gated\n ownerId: SYSTEM_USER_ID,\n });\n \n // Create required system prompts\n const systemPrompts = [\n { name: 'gate-instructions', priority: 10, content: GATE_INSTRUCTIONS },\n { name: 'gate-encouragement', priority: 10, content: GATE_ENCOURAGEMENT },\n { name: 'read-prompts-reminder', priority: 10, content: READ_PROMPTS_REMINDER },\n { name: 'gate-intercept-preamble', priority: 10, content: GATE_INTERCEPT_PREAMBLE },\n { name: 'session-greeting', priority: 10, content: SESSION_GREETING },\n ];\n \n for (const p of systemPrompts) {\n await promptRepo.create({ ...p, projectId: project.id });\n }\n }\n ```\n\n2. Add delete protection in prompt delete endpoint:\n - Check if prompt belongs to mcpctl-system\n - Return 403 error if attempting to delete system prompt\n\n3. Define default content for each system prompt per PRD",
|
||||
"testStrategy": "- Unit test: System project created on first startup\n- Unit test: All 5 system prompts created\n- Unit test: Subsequent startups don't duplicate\n- Unit test: Delete system prompt returns 403\n- Unit test: System prompts have priority 10\n- Unit test: mcpctl-system project has gated=false\n- Integration test: End-to-end bootstrap flow",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"40",
|
||||
"39"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:56:12.064Z"
|
||||
},
|
||||
{
|
||||
"id": "55",
|
||||
"title": "Implement system prompt fetching and caching in mcplocal",
|
||||
"description": "Add functionality to mcplocal router to fetch system prompts from mcpd and cache them with 60s TTL, supporting template variable replacement.",
|
||||
"details": "1. Add system prompt fetching in `/src/mcplocal/src/router.ts`:\n ```typescript\n private systemPromptCache: Map<string, { content: string; expiresAt: number }> = new Map();\n \n async getSystemPrompt(name: string): Promise<string> {\n const cached = this.systemPromptCache.get(name);\n if (cached && cached.expiresAt > Date.now()) {\n return cached.content;\n }\n \n const prompts = await this.mcpdClient.fetch(\n '/api/v1/projects/mcpctl-system/prompts/visible'\n );\n const prompt = prompts.find(p => p.name === name);\n if (!prompt) {\n throw new Error(`System prompt not found: ${name}`);\n }\n \n this.systemPromptCache.set(name, {\n content: prompt.content,\n expiresAt: Date.now() + 60000, // 60s TTL\n });\n \n return prompt.content;\n }\n ```\n\n2. Add template variable replacement:\n ```typescript\n replaceTemplateVariables(content: string, vars: Record<string, string>): string {\n return content\n .replace(/\\{\\{prompt_index\\}\\}/g, vars.prompt_index || '')\n .replace(/\\{\\{project_name\\}\\}/g, vars.project_name || '')\n .replace(/\\{\\{matched_prompts\\}\\}/g, vars.matched_prompts || '')\n .replace(/\\{\\{remaining_prompts\\}\\}/g, vars.remaining_prompts || '');\n }\n ```",
|
||||
"testStrategy": "- Unit test: System prompt fetched from mcpd\n- Unit test: Cached prompt returned within TTL\n- Unit test: Cache miss triggers fresh fetch\n- Unit test: Missing system prompt throws error\n- Unit test: Template variables replaced correctly\n- Unit test: Unknown template variables left as-is\n- Integration test: End-to-end fetch and cache",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"54"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:57:28.917Z"
|
||||
},
|
||||
{
|
||||
"id": "56",
|
||||
"title": "Implement prompt link resolution service",
|
||||
"description": "Create a service that fetches linked prompt content from source MCP servers using the project's service account, with dead link detection.",
|
||||
"details": "1. Create `/src/mcplocal/src/services/link-resolver.service.ts`:\n ```typescript\n export class LinkResolverService {\n constructor(private mcpdClient: McpdClient) {}\n \n async resolveLink(linkTarget: string): Promise<{\n content: string | null;\n status: 'alive' | 'dead' | 'unknown';\n error?: string;\n }> {\n // Parse linkTarget: project/server:resource-uri\n const { project, server, uri } = this.parseLink(linkTarget);\n \n try {\n // Use service account for source project\n const content = await this.fetchResource(project, server, uri);\n return { content, status: 'alive' };\n } catch (error) {\n this.logDeadLink(linkTarget, error);\n return { \n content: null, \n status: 'dead',\n error: error.message \n };\n }\n }\n \n private parseLink(linkTarget: string): { project: string; server: string; uri: string } {\n const match = linkTarget.match(/^([^/]+)\\/([^:]+):(.+)$/);\n if (!match) throw new Error('Invalid link format');\n return { project: match[1], server: match[2], uri: match[3] };\n }\n \n private async fetchResource(project: string, server: string, uri: string): Promise<string> {\n // Call mcpd to fetch resource via service account\n // mcpd routes to the source project's MCP server\n }\n }\n ```\n\n2. Log dead links as errors\n\n3. Cache resolution results",
|
||||
"testStrategy": "- Unit test: Valid link parsed correctly\n- Unit test: Invalid link format throws error\n- Unit test: Successful resolution returns content and status='alive'\n- Unit test: Failed resolution returns status='dead' with error\n- Unit test: Dead link logged as error\n- Unit test: Service account header included in request\n- Integration test: End-to-end link resolution",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"40"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:07:29.026Z"
|
||||
},
|
||||
{
|
||||
"id": "57",
|
||||
"title": "Add linkStatus to prompt GET responses",
|
||||
"description": "Modify the GET /api/v1/prompts endpoint to include linkStatus (alive/dead/unknown) for linked prompts by checking link health.",
|
||||
"details": "1. Update `/src/mcpd/src/routes/prompts.ts` GET endpoint:\n ```typescript\n fastify.get('/api/v1/prompts', async (request, reply) => {\n const prompts = await promptService.findAll(filter);\n \n // Check link status for linked prompts\n const promptsWithStatus = await Promise.all(\n prompts.map(async (p) => {\n if (!p.linkTarget) {\n return { ...p, linkStatus: null };\n }\n const status = await linkResolver.checkLinkHealth(p.linkTarget);\n return { ...p, linkStatus: status };\n })\n );\n \n return reply.send(promptsWithStatus);\n });\n ```\n\n2. Consider caching link health to avoid repeated checks\n\n3. Add `linkStatus` field to prompt response schema:\n - `null` for non-linked prompts\n - `'alive'` for working links\n - `'dead'` for broken links\n - `'unknown'` for unchecked links",
|
||||
"testStrategy": "- Unit test: Non-linked prompt has linkStatus=null\n- Unit test: Linked prompt with working link has linkStatus='alive'\n- Unit test: Linked prompt with broken link has linkStatus='dead'\n- Unit test: Link health cached to avoid repeated checks\n- Unit test: All prompts in response have linkStatus field\n- Integration test: End-to-end GET with linked prompts",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"56"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:09:07.078Z"
|
||||
},
|
||||
{
|
||||
"id": "58",
|
||||
"title": "Add RBAC for prompt link creation",
|
||||
"description": "Implement RBAC checks requiring edit permission on the target project to create prompt links, and auto-create service account permission on the source project.",
|
||||
"details": "1. Update prompt creation in `/src/mcpd/src/services/prompt.service.ts`:\n ```typescript\n async createPrompt(data: CreatePromptInput, userId: string): Promise<Prompt> {\n if (data.linkTarget) {\n // Verify user has edit permission on target project RBAC\n const hasPermission = await this.rbacService.checkPermission(\n userId, data.projectId, 'edit'\n );\n if (!hasPermission) {\n throw new ForbiddenError('Edit permission required to create prompt links');\n }\n \n // Parse link target\n const { project: sourceProject, server, uri } = this.parseLink(data.linkTarget);\n \n // Create service account permission on source project\n await this.rbacService.createServiceAccountPermission(\n data.projectId, // target project\n sourceProject, // source project\n server,\n uri,\n 'read'\n );\n }\n \n return this.promptRepo.create(data);\n }\n ```\n\n2. Clean up service account permission when link is deleted\n\n3. Handle permission denied from source project",
|
||||
"testStrategy": "- Unit test: Link creation requires edit permission\n- Unit test: Link creation without permission throws 403\n- Unit test: Service account permission created on source project\n- Unit test: Service account permission deleted when link deleted\n- Unit test: Non-link prompts skip RBAC checks\n- Integration test: End-to-end link creation with RBAC",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"56"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:09:07.081Z"
|
||||
},
|
||||
{
|
||||
"id": "59",
|
||||
"title": "Update CLI create prompt command for priority and link",
|
||||
"description": "Extend the mcpctl create prompt command to accept --priority (1-10) and --link (project/server:uri) flags.",
|
||||
"details": "1. Update `/src/cli/src/commands/create.ts` for prompt:\n ```typescript\n .command('prompt <name>')\n .description('Create a new prompt')\n .option('-p, --project <name>', 'Project to create prompt in')\n .option('--priority <number>', 'Priority level (1-10, default: 5)', '5')\n .option('--link <target>', 'Link to MCP resource (project/server:uri)')\n .option('-f, --file <path>', 'Read content from file')\n .action(async (name, options) => {\n const priority = parseInt(options.priority, 10);\n if (priority < 1 || priority > 10) {\n console.error('Priority must be between 1 and 10');\n process.exit(1);\n }\n \n let content = '';\n if (options.link) {\n // Linked prompts don't need content (fetched from source)\n content = `[Link: ${options.link}]`;\n } else if (options.file) {\n content = await fs.readFile(options.file, 'utf-8');\n } else {\n content = await promptForContent();\n }\n \n const body = {\n name,\n content,\n projectId: options.project,\n priority,\n linkTarget: options.link,\n };\n \n await api.post('/api/v1/prompts', body);\n });\n ```\n\n2. Validate link format: `project/server:resource-uri`\n\n3. Add shell completions for new flags",
|
||||
"testStrategy": "- Unit test: --priority flag sets prompt priority\n- Unit test: --priority validation (1-10 range)\n- Unit test: --link flag sets linkTarget\n- Unit test: --link validation (format check)\n- Unit test: Linked prompt skips content prompt\n- Unit test: Default priority is 5\n- Integration test: End-to-end create with flags",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"40"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:03:45.972Z"
|
||||
},
|
||||
{
|
||||
"id": "60",
|
||||
"title": "Update CLI get prompt command for -A flag and link columns",
|
||||
"description": "Extend the mcpctl get prompt command with -A (all projects) flag and add link target and status columns to output.",
|
||||
"details": "1. Update `/src/cli/src/commands/get.ts` for prompt:\n ```typescript\n .command('prompt [name]')\n .option('-A, --all-projects', 'Show prompts from all projects')\n .option('-p, --project <name>', 'Filter by project')\n .action(async (name, options) => {\n let url = '/api/v1/prompts';\n if (options.allProjects) {\n url += '?all=true';\n } else if (options.project) {\n url += `?project=${options.project}`;\n }\n \n const prompts = await api.get(url);\n \n // Format table with new columns\n formatPromptsTable(prompts, {\n columns: ['PROJECT', 'NAME', 'PRIORITY', 'LINK', 'STATUS']\n });\n });\n ```\n\n2. Update table formatter to handle link columns:\n ```\n PROJECT NAME PRIORITY LINK STATUS\n homeautomation security-policies 8 - -\n homeautomation architecture-adr 6 system-public/docmost-mcp:docmost://pages/a1 alive\n ```\n\n3. Add shell completions for -A flag",
|
||||
"testStrategy": "- Unit test: -A flag shows all projects\n- Unit test: --project flag filters by project\n- Unit test: PRIORITY column displayed\n- Unit test: LINK column shows linkTarget or -\n- Unit test: STATUS column shows linkStatus or -\n- Unit test: Table formatted correctly\n- Integration test: End-to-end get with flags",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"57",
|
||||
"59"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:09:31.501Z"
|
||||
},
|
||||
{
|
||||
"id": "61",
|
||||
"title": "Update CLI describe project command for gated status",
|
||||
"description": "Extend mcpctl describe project to show gated status, session greeting, and prompt table with priority and link information.",
|
||||
"details": "1. Update `/src/cli/src/commands/get.ts` describe project:\n ```typescript\n async function describeProject(name: string) {\n const project = await api.get(`/api/v1/projects/${name}`);\n const prompts = await api.get(`/api/v1/projects/${name}/prompt-index`);\n const greeting = await getSessionGreeting(name);\n \n console.log(`Name: ${project.name}`);\n console.log(`Gated: ${project.gated}`);\n console.log(`LLM Provider: ${project.llmProvider || '-'}`);\n console.log(`...`);\n console.log();\n console.log(`Session greeting:`);\n console.log(` ${greeting}`);\n console.log();\n console.log(`Prompts:`);\n console.log(` NAME PRIORITY TYPE LINK`);\n for (const p of prompts) {\n const type = p.linkTarget ? 'link' : 'local';\n const link = p.linkTarget || '-';\n console.log(` ${p.name.padEnd(20)} ${p.priority.toString().padEnd(9)} ${type.padEnd(7)} ${link}`);\n }\n }\n ```\n\n2. Fetch session greeting from system prompts or project config",
|
||||
"testStrategy": "- Unit test: Gated status displayed\n- Unit test: Session greeting displayed\n- Unit test: Prompt table with PRIORITY, TYPE, LINK columns\n- Unit test: TYPE shows 'local' or 'link'\n- Unit test: LINK shows target or -\n- Integration test: End-to-end describe project",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"44",
|
||||
"54"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:04:56.320Z"
|
||||
},
|
||||
{
|
||||
"id": "62",
|
||||
"title": "Update CLI edit project command for gated field",
|
||||
"description": "Extend mcpctl edit project to allow editing the gated boolean field.",
|
||||
"details": "1. Update `/src/cli/src/commands/edit.ts` for project:\n ```typescript\n async function editProject(name: string) {\n const project = await api.get(`/api/v1/projects/${name}`);\n \n // Add gated to editable fields\n const yaml = `\n name: ${project.name}\n description: ${project.description}\n gated: ${project.gated}\n llmProvider: ${project.llmProvider || ''}\n ...`;\n \n const edited = await openEditor(yaml);\n const parsed = YAML.parse(edited);\n \n // Validate gated is boolean\n if (typeof parsed.gated !== 'boolean') {\n console.error('gated must be true or false');\n process.exit(1);\n }\n \n await api.put(`/api/v1/projects/${name}`, parsed);\n }\n ```\n\n2. Update project validation schema to accept gated\n\n3. Handle conversion from string 'true'/'false' to boolean",
|
||||
"testStrategy": "- Unit test: Gated field appears in editor YAML\n- Unit test: Gated field saved on edit\n- Unit test: Boolean validation (true/false only)\n- Unit test: String 'true'/'false' converted to boolean\n- Integration test: End-to-end edit project gated",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"39"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:03:46.657Z"
|
||||
},
|
||||
{
|
||||
"id": "63",
|
||||
"title": "Add unit tests for prompt priority and link CRUD",
|
||||
"description": "Create comprehensive unit tests for all prompt CRUD operations with the new priority and linkTarget fields.",
|
||||
"details": "1. Add tests in `/src/mcpd/tests/services/prompt-service.test.ts`:\n ```typescript\n describe('Prompt Priority', () => {\n it('creates prompt with explicit priority', async () => {\n const prompt = await service.createPrompt({ ...data, priority: 8 });\n expect(prompt.priority).toBe(8);\n });\n \n it('uses default priority 5 when not specified', async () => {\n const prompt = await service.createPrompt(data);\n expect(prompt.priority).toBe(5);\n });\n \n it('validates priority range 1-10', async () => {\n await expect(service.createPrompt({ ...data, priority: 11 }))\n .rejects.toThrow();\n });\n \n it('updates priority', async () => {\n const updated = await service.updatePrompt(id, { priority: 3 });\n expect(updated.priority).toBe(3);\n });\n });\n \n describe('Prompt Links', () => {\n it('creates linked prompt', async () => {\n const prompt = await service.createPrompt({\n ...data,\n linkTarget: 'project/server:uri'\n });\n expect(prompt.linkTarget).toBe('project/server:uri');\n });\n \n it('rejects invalid link format', async () => {\n await expect(service.createPrompt({\n ...data,\n linkTarget: 'invalid'\n })).rejects.toThrow();\n });\n \n it('linkTarget is immutable on update', async () => {\n // linkTarget not accepted in update schema\n });\n });\n ```",
|
||||
"testStrategy": "This task IS the test implementation. Verify:\n- All priority CRUD tests pass\n- All link CRUD tests pass\n- Validation tests cover edge cases\n- Tests use proper mocking patterns\n- Coverage meets project standards",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"40",
|
||||
"41"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:52:53.091Z"
|
||||
},
|
||||
{
|
||||
"id": "64",
|
||||
"title": "Add unit tests for tag matching algorithm",
|
||||
"description": "Create comprehensive unit tests for the deterministic tag matching algorithm covering score calculation, byte budget, and priority handling.",
|
||||
"details": "1. Add tests in `/src/mcplocal/tests/services/tag-matcher.test.ts`:\n ```typescript\n describe('TagMatcherService', () => {\n describe('score calculation', () => {\n it('priority 10 prompts have infinite score', () => {\n const score = matcher.computeScore(['any'], { priority: 10, ... });\n expect(score).toBe(Infinity);\n });\n \n it('score = matching_tags * priority', () => {\n const score = matcher.computeScore(\n ['tag1', 'tag2'],\n { priority: 5, summary: 'tag1 tag2', chapters: [] }\n );\n expect(score).toBe(10); // 2 tags * 5 priority\n });\n });\n \n describe('matching', () => {\n it('matches case-insensitively', () => {\n const matches = matcher.matchesPrompt('ZIGBEE', { summary: 'zigbee setup' });\n expect(matches).toBe(true);\n });\n \n it('matches substring in summary', () => { ... });\n it('matches substring in chapters', () => { ... });\n });\n \n describe('byte budget', () => {\n it('includes full content until budget exhausted', () => { ... });\n it('matched prompts beyond budget become index entries', () => { ... });\n it('non-matched prompts listed as names only', () => { ... });\n });\n });\n ```",
|
||||
"testStrategy": "This task IS the test implementation. Verify:\n- Score calculation tests pass\n- Matching tests cover all cases\n- Byte budget tests verify allocation\n- Edge cases handled (empty tags, no prompts, etc.)\n- Tests are deterministic",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"45"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:51:03.827Z"
|
||||
},
|
||||
{
|
||||
"id": "65",
|
||||
"title": "Add unit tests for gating state machine",
|
||||
"description": "Create comprehensive unit tests for the session gating state machine covering all transitions and edge cases.",
|
||||
"details": "1. Add tests in `/src/mcplocal/tests/router-gating.test.ts`:\n ```typescript\n describe('Gating State Machine', () => {\n describe('initial state', () => {\n it('starts gated for gated project', () => {\n const router = createRouter({ gated: true });\n const state = router.getSessionState('session1');\n expect(state.gated).toBe(true);\n });\n \n it('starts ungated for non-gated project', () => {\n const router = createRouter({ gated: false });\n const state = router.getSessionState('session1');\n expect(state.gated).toBe(false);\n });\n });\n \n describe('begin_session transition', () => {\n it('ungates session on successful begin_session', async () => {\n const router = createGatedRouter();\n await router.handleBeginSession('session1', { tags: ['test'] });\n expect(router.getSessionState('session1').gated).toBe(false);\n });\n \n it('returns matched prompts', async () => { ... });\n it('sends notifications/tools/list_changed', async () => { ... });\n });\n \n describe('intercept transition', () => {\n it('ungates session on tool call intercept', async () => { ... });\n it('extracts keywords from tool call', async () => { ... });\n it('injects briefing with tool result', async () => { ... });\n });\n \n describe('tools/list behavior', () => {\n it('returns only begin_session while gated', async () => { ... });\n it('returns all tools + read_prompts after ungating', async () => { ... });\n });\n });\n ```",
|
||||
"testStrategy": "This task IS the test implementation. Verify:\n- Initial state tests pass\n- Transition tests cover happy paths\n- Edge case tests (already ungated, etc.)\n- Notification tests verify signals sent\n- Tests use proper mocking",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"50",
|
||||
"52"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:51:03.832Z"
|
||||
},
|
||||
{
|
||||
"id": "66",
|
||||
"title": "Add unit tests for LLM prompt selection",
|
||||
"description": "Create unit tests for the LLM-based prompt selection service covering LLM interactions, fallback behavior, and priority 10 handling.",
|
||||
"details": "1. Add tests in `/src/mcplocal/tests/services/llm-prompt-selector.test.ts`:\n ```typescript\n describe('LlmPromptSelectorService', () => {\n describe('priority 10 handling', () => {\n it('always includes priority 10 prompts', async () => {\n const result = await selector.selectPrompts(['unrelated'], promptIndex);\n expect(result.priority10).toContain(priority10Prompt);\n });\n });\n \n describe('LLM selection', () => {\n it('sends tags and index to heavy LLM', async () => {\n await selector.selectPrompts(['zigbee', 'mqtt'], promptIndex);\n expect(mockLlm.complete).toHaveBeenCalledWith(\n expect.stringContaining('zigbee')\n );\n });\n \n it('parses LLM response correctly', async () => {\n mockLlm.complete.mockResolvedValue(\n '[{\"name\": \"prompt1\", \"reason\": \"relevant\"}]'\n );\n const result = await selector.selectPrompts(['test'], promptIndex);\n expect(result.selected[0].name).toBe('prompt1');\n });\n });\n \n describe('fallback behavior', () => {\n it('falls back to tag matcher on LLM error', async () => { ... });\n it('falls back on LLM timeout', async () => { ... });\n it('falls back when no LLM available', async () => { ... });\n });\n \n describe('summary generation', () => {\n it('generates missing summaries with fast LLM', async () => { ... });\n });\n });\n ```",
|
||||
"testStrategy": "This task IS the test implementation. Verify:\n- Priority 10 tests pass\n- LLM interaction tests use proper mocks\n- Fallback tests cover all error scenarios\n- Summary generation tests pass\n- Response parsing handles edge cases",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"46"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:51:03.836Z"
|
||||
},
|
||||
{
|
||||
"id": "67",
|
||||
"title": "Add integration tests for gated session flow",
|
||||
"description": "Create end-to-end integration tests for the complete gated session flow including connect, begin_session, tool calls, and read_prompts.",
|
||||
"details": "1. Add tests in `/src/mcplocal/tests/integration/gated-flow.test.ts`:\n ```typescript\n describe('Gated Session Flow Integration', () => {\n let app: FastifyInstance;\n let mcpClient: McpClient;\n \n beforeAll(async () => {\n app = await createTestApp();\n // Seed test project with gated=true and test prompts\n });\n \n describe('end-to-end gated flow', () => {\n it('connect → begin_session with tags → tools available → correct prompts', async () => {\n // 1. Connect to MCP endpoint\n const session = await mcpClient.connect(app, 'test-project');\n \n // 2. Verify only begin_session available\n const toolsBefore = await session.listTools();\n expect(toolsBefore.map(t => t.name)).toEqual(['begin_session']);\n \n // 3. Call begin_session\n const briefing = await session.callTool('begin_session', {\n tags: ['test', 'integration']\n });\n expect(briefing).toContain('matched prompt content');\n \n // 4. Verify all tools now available\n const toolsAfter = await session.listTools();\n expect(toolsAfter.map(t => t.name)).toContain('read_prompts');\n });\n });\n \n describe('end-to-end intercept flow', () => {\n it('connect → skip begin_session → call tool → keywords extracted → briefing injected', async () => { ... });\n });\n \n describe('end-to-end read_prompts', () => {\n it('after ungating → request more context → additional prompts → no duplicates', async () => { ... });\n });\n });\n ```",
|
||||
"testStrategy": "This task IS the test implementation. Verify:\n- Happy path tests pass\n- Intercept path tests pass\n- read_prompts deduplication works\n- Tests use realistic data\n- Tests clean up properly",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"65"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T22:51:03.840Z"
|
||||
},
|
||||
{
|
||||
"id": "68",
|
||||
"title": "Add integration tests for prompt links",
|
||||
"description": "Create end-to-end integration tests for prompt link creation, resolution, and dead link detection.",
|
||||
"details": "1. Add tests in `/src/mcplocal/tests/integration/prompt-links.test.ts`:\n ```typescript\n describe('Prompt Links Integration', () => {\n describe('link creation', () => {\n it('creates link with RBAC permission', async () => {\n // Setup: user with edit permission on target project\n const prompt = await api.post('/api/v1/prompts', {\n name: 'linked-prompt',\n content: '[Link]',\n projectId: targetProject.id,\n linkTarget: 'source-project/server:uri'\n });\n expect(prompt.linkTarget).toBe('source-project/server:uri');\n });\n \n it('rejects link creation without RBAC permission', async () => { ... });\n });\n \n describe('link resolution', () => {\n it('fetches content from source server', async () => { ... });\n it('uses service account for RBAC', async () => { ... });\n });\n \n describe('dead link lifecycle', () => {\n it('detects dead link when source unavailable', async () => {\n // Kill source server\n const prompts = await api.get('/api/v1/prompts');\n const linked = prompts.find(p => p.linkTarget);\n expect(linked.linkStatus).toBe('dead');\n });\n \n it('recovers when source restored', async () => { ... });\n });\n });\n ```",
|
||||
"testStrategy": "This task IS the test implementation. Verify:\n- RBAC tests cover permission scenarios\n- Resolution tests verify content fetched\n- Dead link tests cover full lifecycle\n- Tests properly mock/control source servers\n- Tests clean up resources",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"57",
|
||||
"58"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:12:22.348Z"
|
||||
},
|
||||
{
|
||||
"id": "69",
|
||||
"title": "Add CLI unit tests for new prompt and project flags",
|
||||
"description": "Create unit tests for the new CLI flags: --priority, --link for prompts, -A for get, and gated field for projects.",
|
||||
"details": "1. Add tests in `/src/cli/tests/commands/prompt.test.ts`:\n ```typescript\n describe('create prompt command', () => {\n it('--priority sets prompt priority', async () => {\n await cli('create prompt test --priority 8');\n expect(mockApi.post).toHaveBeenCalledWith(\n '/api/v1/prompts',\n expect.objectContaining({ priority: 8 })\n );\n });\n \n it('--priority validates range 1-10', async () => {\n await expect(cli('create prompt test --priority 15'))\n .rejects.toThrow('Priority must be between 1 and 10');\n });\n \n it('--link sets linkTarget', async () => {\n await cli('create prompt test --link proj/srv:uri');\n expect(mockApi.post).toHaveBeenCalledWith(\n '/api/v1/prompts',\n expect.objectContaining({ linkTarget: 'proj/srv:uri' })\n );\n });\n });\n \n describe('get prompt command', () => {\n it('-A shows all projects', async () => {\n await cli('get prompt -A');\n expect(mockApi.get).toHaveBeenCalledWith('/api/v1/prompts?all=true');\n });\n });\n ```\n\n2. Add tests for project gated field editing\n\n3. Add tests for describe project output",
|
||||
"testStrategy": "This task IS the test implementation. Verify:\n- Flag parsing tests pass\n- Validation tests cover edge cases\n- API call tests verify correct parameters\n- Output formatting tests verify columns\n- Tests mock API properly",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"59",
|
||||
"60",
|
||||
"61",
|
||||
"62"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:12:22.352Z"
|
||||
},
|
||||
{
|
||||
"id": "70",
|
||||
"title": "Add shell completions for new CLI flags",
|
||||
"description": "Update shell completion scripts (bash, zsh, fish) to include completions for new flags: --priority, --link, -A, and gated values.",
|
||||
"details": "1. Update `/completions/mcpctl.fish`:\n ```fish\n # create prompt completions\n complete -c mcpctl -n '__fish_seen_subcommand_from create; and __fish_seen_subcommand_from prompt' -l priority -d 'Priority level (1-10)' -a '(seq 1 10)'\n complete -c mcpctl -n '__fish_seen_subcommand_from create; and __fish_seen_subcommand_from prompt' -l link -d 'Link to MCP resource (project/server:uri)'\n \n # get prompt completions \n complete -c mcpctl -n '__fish_seen_subcommand_from get; and __fish_seen_subcommand_from prompt' -s A -l all-projects -d 'Show prompts from all projects'\n ```\n\n2. Update bash completions similarly\n\n3. Update zsh completions similarly\n\n4. Add dynamic completion for priority values (1-10)",
|
||||
"testStrategy": "- Manual test: Fish completions suggest --priority with values 1-10\n- Manual test: Fish completions suggest --link flag\n- Manual test: Fish completions suggest -A/--all-projects\n- Manual test: Bash completions work similarly\n- Manual test: Zsh completions work similarly",
|
||||
"priority": "low",
|
||||
"dependencies": [
|
||||
"59",
|
||||
"60"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-02-25T23:12:22.363Z"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"lastModified": "2026-02-21T18:52:29.084Z",
|
||||
"taskCount": 36,
|
||||
"completedCount": 33,
|
||||
"lastModified": "2026-02-25T23:12:22.364Z",
|
||||
"taskCount": 70,
|
||||
"completedCount": 67,
|
||||
"tags": [
|
||||
"master"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user