diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..218eade2 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": ["tsx", "src/cli/mcp-server.ts", "--watch"] + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 7e28b2e5..1869d8c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,6 +137,130 @@ See the **Context Gathering Protocol** section above for mandatory session start - Output modifiers (`--names-only`, `--count`, `--fields`) compose with any list/query command. - `pnpm` outputs a banner to stdout. For clean JSON piping, use `npx tsx src/cli/process-api.ts` directly. +### MCP Server — Native AI Context Tools + +When the MCP server is running, **use `dp_*` tools instead of CLI commands** (`pnpm process:query --`). The MCP server keeps the MasterDataset in memory — tool calls dispatch in sub-milliseconds vs 2-5 seconds for CLI subprocess invocations. All 25 tools wrap the same ProcessStateAPI methods available via CLI. + +#### Session Start (MCP Protocol) + +Use these tools at the start of every PR or implementation session, in order: + +| Step | MCP Tool | What You Get | CLI Equivalent | +| ---- | ------------------- | ------------------------------------------- | ------------------------------------------------- | +| 1 | `dp_overview` | Project health, active phases, blockers | `pnpm process:query -- overview` | +| 2 | `dp_scope_validate` | Pre-flight: FSM violations, missing deps | `pnpm process:query -- scope-validate

` | +| 3 | `dp_context` | Curated context bundle for the session | `pnpm process:query -- context

--session ` | +| 4 | `dp_files` | File reading list with implementation paths | `pnpm process:query -- files

--related` | + +Steps 1-2 can run in parallel (no dependencies between them). + +#### Tool Reference + +**Session-Aware Tools** — text output, use for workflow: + +| Tool | Input | Description | +| ------------------- | ------------------- | ---------------------------------------------- | +| `dp_overview` | _(none)_ | Progress %, active phases, blocking chains | +| `dp_context` | `name`, `session?` | Curated context (planning/design/implement) | +| `dp_files` | `name` | Ordered file list with roles | +| `dp_dep_tree` | `name`, `maxDepth?` | Dependency chain with status per dep | +| `dp_scope_validate` | `name`, `session` | PASS/BLOCKED/WARN pre-flight verdict | +| `dp_handoff` | `name`, `session?` | Session-end state for multi-session continuity | + +**Data Query Tools** — JSON output, use for structured lookups: + +| Tool | Input | Description | +| -------------- | -------------------------------------------------------- | -------------------------------------------------- | +| `dp_status` | _(none)_ | Pattern counts by status, completion % | +| `dp_pattern` | `name` | Full metadata: deliverables, relationships, shapes | +| `dp_list` | `status?`, `phase?`, `category?`, `namesOnly?`, `count?` | Filtered pattern listing | +| `dp_search` | `query` | Fuzzy name search with similarity scores | +| `dp_rules` | `pattern?`, `onlyInvariants?` | Business rules from Gherkin Rule: blocks | +| `dp_tags` | _(none)_ | Tag usage counts across all sources | +| `dp_sources` | _(none)_ | File inventory by type (TS, Gherkin, stubs) | +| `dp_stubs` | `unresolved?` | Design stubs with resolution status | +| `dp_decisions` | `name?` | AD-N/DD-N design decisions from stubs | + +**Architecture Tools** — JSON output, use for dependency and structure analysis: + +| Tool | Input | Description | +| ---------------------- | -------- | ------------------------------------------- | +| `dp_arch_context` | `name?` | Bounded contexts with member patterns | +| `dp_arch_layer` | `name?` | Architecture layers with member patterns | +| `dp_arch_neighborhood` | `name` | Uses, used-by, same-context peers | +| `dp_arch_blocking` | _(none)_ | Patterns blocked by incomplete dependencies | +| `dp_arch_dangling` | _(none)_ | Broken references to nonexistent patterns | +| `dp_arch_coverage` | `path?` | Annotation coverage % and unused taxonomy | +| `dp_unannotated` | `path?` | Files missing @libar-docs annotations | + +**Server Management:** + +| Tool | Description | +| ------------ | ------------------------------------------------------ | +| `dp_rebuild` | Force dataset rebuild from current source files | +| `dp_config` | Show source globs, base dir, build time, pattern count | +| `dp_help` | List all available tools with descriptions | + +#### Common Recipes + +| Goal | Tools | +| ---------------------------------- | ------------------------------------------------------------------ | +| What patterns are blocking? | `dp_arch_blocking` | +| Understand a pattern before coding | `dp_context` (name, session) + `dp_scope_validate` (name, session) | +| Find business rules for a feature | `dp_rules` with `pattern` filter | +| Check annotation gaps | `dp_arch_coverage` or `dp_unannotated` | +| Explore a bounded context | `dp_arch_context` with name | +| Find what depends on a pattern | `dp_arch_neighborhood` with name | +| List all roadmap patterns | `dp_list` with `status: "roadmap"` | +| Search by partial name | `dp_search` with query | + +#### Configuration + +The MCP server is configured via `.mcp.json` in the project root: + +```json +{ + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": ["dp-mcp-server", "--watch"] + } + } +} +``` + +For monorepo setups with explicit source globs: + +```json +{ + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": [ + "dp-mcp-server", + "--watch", + "--input", + "packages/core/src/**/*.ts", + "--input", + "packages/api/src/**/*.ts", + "--features", + "specs/**/*.feature" + ] + } + } +} +``` + +The `--watch` flag enables auto-rebuild when `.ts` or `.feature` files change (500ms debounce). Without it, use `dp_rebuild` after annotation changes. + +#### Tips + +- `dp_rules` without a `pattern` filter returns a compact summary (totals + rule names + per-area counts) — unfiltered output is capped to prevent context overflow. +- `dp_pattern` returns full metadata (~66KB for completed patterns) — prefer `dp_context` with a session type for interactive sessions. +- `dp_search` uses fuzzy matching — partial names work (e.g., "MCP" matches all MCP-related patterns). +- `dp_list` filters compose: `status` + `phase` + `category` narrow results cumulatively. +- Session-aware tools return formatted text (like CLI output). Data and architecture tools return JSON. + --- ## Architecture diff --git a/_claude-md/api/mcp-server.md b/_claude-md/api/mcp-server.md new file mode 100644 index 00000000..fa7d26d8 --- /dev/null +++ b/_claude-md/api/mcp-server.md @@ -0,0 +1,123 @@ +### MCP Server — Native AI Context Tools + +When the MCP server is running, **use `dp_*` tools instead of CLI commands** (`pnpm process:query --`). The MCP server keeps the MasterDataset in memory — tool calls dispatch in sub-milliseconds vs 2-5 seconds for CLI subprocess invocations. All 25 tools wrap the same ProcessStateAPI methods available via CLI. + +#### Session Start (MCP Protocol) + +Use these tools at the start of every PR or implementation session, in order: + +| Step | MCP Tool | What You Get | CLI Equivalent | +| ---- | ------------------- | ------------------------------------------- | ------------------------------------------------- | +| 1 | `dp_overview` | Project health, active phases, blockers | `pnpm process:query -- overview` | +| 2 | `dp_scope_validate` | Pre-flight: FSM violations, missing deps | `pnpm process:query -- scope-validate

` | +| 3 | `dp_context` | Curated context bundle for the session | `pnpm process:query -- context

--session ` | +| 4 | `dp_files` | File reading list with implementation paths | `pnpm process:query -- files

--related` | + +Steps 1-2 can run in parallel (no dependencies between them). + +#### Tool Reference + +**Session-Aware Tools** — text output, use for workflow: + +| Tool | Input | Description | +| ------------------- | ------------------- | ---------------------------------------------- | +| `dp_overview` | _(none)_ | Progress %, active phases, blocking chains | +| `dp_context` | `name`, `session?` | Curated context (planning/design/implement) | +| `dp_files` | `name` | Ordered file list with roles | +| `dp_dep_tree` | `name`, `maxDepth?` | Dependency chain with status per dep | +| `dp_scope_validate` | `name`, `session` | PASS/BLOCKED/WARN pre-flight verdict | +| `dp_handoff` | `name`, `session?` | Session-end state for multi-session continuity | + +**Data Query Tools** — JSON output, use for structured lookups: + +| Tool | Input | Description | +| -------------- | -------------------------------------------------------- | -------------------------------------------------- | +| `dp_status` | _(none)_ | Pattern counts by status, completion % | +| `dp_pattern` | `name` | Full metadata: deliverables, relationships, shapes | +| `dp_list` | `status?`, `phase?`, `category?`, `namesOnly?`, `count?` | Filtered pattern listing | +| `dp_search` | `query` | Fuzzy name search with similarity scores | +| `dp_rules` | `pattern?`, `onlyInvariants?` | Business rules from Gherkin Rule: blocks | +| `dp_tags` | _(none)_ | Tag usage counts across all sources | +| `dp_sources` | _(none)_ | File inventory by type (TS, Gherkin, stubs) | +| `dp_stubs` | `unresolved?` | Design stubs with resolution status | +| `dp_decisions` | `name?` | AD-N/DD-N design decisions from stubs | + +**Architecture Tools** — JSON output, use for dependency and structure analysis: + +| Tool | Input | Description | +| ---------------------- | -------- | ------------------------------------------- | +| `dp_arch_context` | `name?` | Bounded contexts with member patterns | +| `dp_arch_layer` | `name?` | Architecture layers with member patterns | +| `dp_arch_neighborhood` | `name` | Uses, used-by, same-context peers | +| `dp_arch_blocking` | _(none)_ | Patterns blocked by incomplete dependencies | +| `dp_arch_dangling` | _(none)_ | Broken references to nonexistent patterns | +| `dp_arch_coverage` | `path?` | Annotation coverage % and unused taxonomy | +| `dp_unannotated` | `path?` | Files missing @libar-docs annotations | + +**Server Management:** + +| Tool | Description | +| ------------ | ------------------------------------------------------ | +| `dp_rebuild` | Force dataset rebuild from current source files | +| `dp_config` | Show source globs, base dir, build time, pattern count | +| `dp_help` | List all available tools with descriptions | + +#### Common Recipes + +| Goal | Tools | +| ---------------------------------- | ------------------------------------------------------------------ | +| What patterns are blocking? | `dp_arch_blocking` | +| Understand a pattern before coding | `dp_context` (name, session) + `dp_scope_validate` (name, session) | +| Find business rules for a feature | `dp_rules` with `pattern` filter | +| Check annotation gaps | `dp_arch_coverage` or `dp_unannotated` | +| Explore a bounded context | `dp_arch_context` with name | +| Find what depends on a pattern | `dp_arch_neighborhood` with name | +| List all roadmap patterns | `dp_list` with `status: "roadmap"` | +| Search by partial name | `dp_search` with query | + +#### Configuration + +The MCP server is configured via `.mcp.json` in the project root: + +```json +{ + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": ["dp-mcp-server", "--watch"] + } + } +} +``` + +For monorepo setups with explicit source globs: + +```json +{ + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": [ + "dp-mcp-server", + "--watch", + "--input", + "packages/core/src/**/*.ts", + "--input", + "packages/api/src/**/*.ts", + "--features", + "specs/**/*.feature" + ] + } + } +} +``` + +The `--watch` flag enables auto-rebuild when `.ts` or `.feature` files change (500ms debounce). Without it, use `dp_rebuild` after annotation changes. + +#### Tips + +- `dp_rules` without a `pattern` filter returns a compact summary (totals + rule names + per-area counts) — unfiltered output is capped to prevent context overflow. +- `dp_pattern` returns full metadata (~66KB for completed patterns) — prefer `dp_context` with a session type for interactive sessions. +- `dp_search` uses fuzzy matching — partial names work (e.g., "MCP" matches all MCP-related patterns). +- `dp_list` filters compose: `status` + `phase` + `category` narrow results cumulatively. +- Session-aware tools return formatted text (like CLI output). Data and architecture tools return JSON. diff --git a/_claude-md/metadata.json b/_claude-md/metadata.json index 4581dd4d..6c0589ce 100644 --- a/_claude-md/metadata.json +++ b/_claude-md/metadata.json @@ -46,6 +46,11 @@ "path": "api/data-api-cli.md", "tags": ["core"], "description": "Data API CLI for querying delivery process state" + }, + { + "path": "api/mcp-server.md", + "tags": ["core"], + "description": "MCP server tools for native AI agent access to ProcessStateAPI" } ] }, diff --git a/delivery-process/design-reviews/mcp-server-integration.md b/delivery-process/design-reviews/mcp-server-integration.md new file mode 100644 index 00000000..4cad352a --- /dev/null +++ b/delivery-process/design-reviews/mcp-server-integration.md @@ -0,0 +1,185 @@ +# Design Review: MCPServerIntegration + +**Purpose:** Auto-generated design review with sequence and component diagrams +**Detail Level:** Design review artifact from sequence annotations + +--- + +**Pattern:** MCPServerIntegration | **Phase:** Phase 46 | **Status:** active | **Orchestrator:** mcp-server | **Steps:** 5 | **Participants:** 4 + +**Source:** `delivery-process/specs/mcp-server-integration.feature` + +--- + +## Annotation Convention + +This design review is generated from the following annotations: + +| Tag | Level | Format | Purpose | +| --------------------- | -------- | ------ | ---------------------------------- | +| sequence-orchestrator | Feature | value | Identifies the coordinator module | +| sequence-step | Rule | number | Explicit execution ordering | +| sequence-module | Rule | csv | Maps Rule to deliverable module(s) | +| sequence-error | Scenario | flag | Marks scenario as error/alt path | + +Description markers: `**Input:**` and `**Output:**` in Rule descriptions define data flow types for sequence diagram call arrows and component diagram edges. + +--- + +## Sequence Diagram — Runtime Interaction Flow + +Generated from: `@libar-docs-sequence-step`, `@libar-docs-sequence-module`, `@libar-docs-sequence-error`, `**Input:**`/`**Output:**` markers, and `@libar-docs-sequence-orchestrator` on the Feature. + +```mermaid +sequenceDiagram + participant User + participant mcp_server as "mcp-server.ts" + participant pipeline_session as "pipeline-session.ts" + participant tool_registry as "tool-registry.ts" + participant file_watcher as "file-watcher.ts" + + User->>mcp_server: invoke + + Note over mcp_server: Rule 1 — The MCP server communicates over stdio using JSON-RPC. It builds the pipeline once during initialization, then enters a request-response loop. No non-MCP output is written to stdout (no console.log, no pnpm banners). + + mcp_server->>+pipeline_session: SessionOptions + pipeline_session-->>-mcp_server: PipelineSession + + alt Server starts with explicit input globs + mcp_server-->>User: error + mcp_server->>mcp_server: exit(1) + end + + Note over mcp_server: Rule 2 — Every CLI subcommand is registered as an MCP tool with a JSON Schema describing its input parameters. Tool names use snake_case with a "dp_" prefix to avoid collisions with other MCP servers. + + mcp_server->>+tool_registry: PipelineSession + tool_registry-->>-mcp_server: RegisteredTools + + alt Tool call with missing required parameter returns error + mcp_server-->>User: error + mcp_server->>mcp_server: exit(1) + end + + Note over mcp_server: Rule 3 — The pipeline runs exactly once during server initialization. All subsequent tool calls read from in-memory MasterDataset. A manual rebuild can be triggered via a "dp_rebuild" tool, and overlapping rebuild requests coalesce so the final in-memory session reflects the newest completed build. + + mcp_server->>+pipeline_session: ToolCallRequest + pipeline_session-->>-mcp_server: ToolCallResult + + alt Concurrent reads during rebuild use previous dataset + mcp_server-->>User: error + mcp_server->>mcp_server: exit(1) + end + + Note over mcp_server: Rule 4 — When —watch is enabled, changes to source files trigger an automatic pipeline rebuild. Multiple rapid changes are debounced into a single rebuild (default 500ms window). + + mcp_server->>+file_watcher: FileChangeEvent + file_watcher-->>-mcp_server: PipelineSession + + alt Rebuild failure during watch does not crash server + mcp_server-->>User: error + mcp_server->>mcp_server: exit(1) + end + + Note over mcp_server: Rule 5 — The server works with .mcp.json (Claude Code), claude_desktop_config.json (Claude Desktop), and any MCP client. It accepts —input, —features, —base-dir args, auto-detects delivery-process.config.ts, and reports the package version accurately through the CLI. + + mcp_server->>+mcp_server: CLIArgs + mcp_server-->>-mcp_server: McpServerOptions + + alt No config file and no explicit globs + mcp_server-->>User: error + mcp_server->>mcp_server: exit(1) + end + +``` + +--- + +## Component Diagram — Types and Data Flow + +Generated from: `@libar-docs-sequence-module` (nodes), `**Input:**`/`**Output:**` (edges and type shapes), deliverables table (locations), and `sequence-step` (grouping). + +```mermaid +graph LR + subgraph phase_1["Phase 1: SessionOptions"] + phase_1_pipeline_session["pipeline-session.ts"] + end + + subgraph phase_2["Phase 2: PipelineSession"] + phase_2_tool_registry["tool-registry.ts"] + end + + subgraph phase_3["Phase 3: ToolCallRequest"] + phase_3_pipeline_session["pipeline-session.ts"] + end + + subgraph phase_4["Phase 4: FileChangeEvent"] + phase_4_file_watcher["file-watcher.ts"] + end + + subgraph phase_5["Phase 5: CLIArgs"] + phase_5_mcp_server["mcp-server.ts"] + end + + subgraph orchestrator["Orchestrator"] + mcp_server["mcp-server.ts"] + end + + subgraph types["Key Types"] + PipelineSession{{"PipelineSession\n-----------\ndataset\napi\nregistry\nbaseDir\nsourceGlobs\nbuildTimeMs"}} + RegisteredTools{{"RegisteredTools\n-----------\n25 tools with dp_ prefix\nZod input schemas\nhandler functions"}} + ToolCallResult{{"ToolCallResult\n-----------\ncontent\nisError"}} + McpServerOptions{{"McpServerOptions\n-----------\nparsed options merged with config defaults"}} + end + + phase_1_pipeline_session -->|"PipelineSession"| mcp_server + phase_2_tool_registry -->|"RegisteredTools"| mcp_server + phase_3_pipeline_session -->|"ToolCallResult"| mcp_server + phase_4_file_watcher -->|"PipelineSession"| mcp_server + phase_5_mcp_server -->|"McpServerOptions"| mcp_server + mcp_server -->|"SessionOptions"| phase_1_pipeline_session + mcp_server -->|"PipelineSession"| phase_2_tool_registry + mcp_server -->|"ToolCallRequest"| phase_3_pipeline_session + mcp_server -->|"FileChangeEvent"| phase_4_file_watcher + mcp_server -->|"CLIArgs"| phase_5_mcp_server +``` + +--- + +## Key Type Definitions + +| Type | Fields | Produced By | Consumed By | +| ------------------ | --------------------------------------------------------------- | ------------------------------ | ------------- | +| `PipelineSession` | dataset, api, registry, baseDir, sourceGlobs, buildTimeMs | pipeline-session, file-watcher | tool-registry | +| `RegisteredTools` | 25 tools with dp\_ prefix, Zod input schemas, handler functions | tool-registry | | +| `ToolCallResult` | content, isError | pipeline-session | | +| `McpServerOptions` | parsed options merged with config defaults | mcp-server | | + +--- + +## Design Questions + +Verify these design properties against the diagrams above: + +| # | Question | Auto-Check | Diagram | +| ---- | ------------------------------------ | ------------------------------- | --------- | +| DQ-1 | Is the execution ordering correct? | 5 steps in monotonic order | Sequence | +| DQ-2 | Are all interfaces well-defined? | 4 distinct types across 5 steps | Component | +| DQ-3 | Is error handling complete? | 5 error paths identified | Sequence | +| DQ-4 | Is data flow unidirectional? | Review component diagram edges | Component | +| DQ-5 | Does validation prove the full path? | Review final step | Both | + +--- + +## Findings + +Record design observations from reviewing the diagrams above. Each finding should reference which diagram revealed it and its impact on the spec. + +| # | Finding | Diagram Source | Impact on Spec | +| --- | ------------------------------------------- | -------------- | -------------- | +| F-1 | (Review the diagrams and add findings here) | — | — | + +--- + +## Summary + +The MCPServerIntegration design review covers 5 sequential steps across 4 participants with 4 key data types and 5 error paths. diff --git a/delivery-process/specs/mcp-server-integration.feature b/delivery-process/specs/mcp-server-integration.feature index 6a557fe6..360741fa 100644 --- a/delivery-process/specs/mcp-server-integration.feature +++ b/delivery-process/specs/mcp-server-integration.feature @@ -1,6 +1,6 @@ @libar-docs @libar-docs-pattern:MCPServerIntegration -@libar-docs-status:roadmap +@libar-docs-status:active @libar-docs-phase:46 @libar-docs-product-area:DataAPI @libar-docs-effort:3d @@ -8,6 +8,7 @@ @libar-docs-depends-on:DataAPICLIErgonomics @libar-docs-see-also:DataAPIPlatformIntegration,DataAPICLIErgonomics @libar-docs-business-value:native-claude-code-tool-integration-with-zero-subprocess-overhead +@libar-docs-sequence-orchestrator:mcp-server Feature: MCP Server Integration **Problem:** @@ -28,13 +29,15 @@ Feature: MCP Server Integration Background: Deliverables Given the following deliverables: | Deliverable | Status | Location | - | MCP server entry point and lifecycle | pending | src/mcp/server.ts | - | Tool registry with JSON Schema generation | pending | src/mcp/tool-registry.ts | - | Pipeline session manager | pending | src/mcp/pipeline-session.ts | - | File watcher with debounced rebuild | pending | src/mcp/file-watcher.ts | - | MCP server bin entry | pending | src/cli/mcp-server.ts | - | MCP configuration documentation | pending | docs/MCP-SETUP.md | - + | MCP server entry point and lifecycle | complete | src/mcp/server.ts | + | Tool registry with JSON Schema generation | complete | src/mcp/tool-registry.ts | + | Pipeline session manager | complete | src/mcp/pipeline-session.ts | + | File watcher with debounced rebuild | complete | src/mcp/file-watcher.ts | + | MCP server bin entry | complete | src/cli/mcp-server.ts | + | MCP configuration documentation | complete | docs/MCP-SETUP.md | + + @libar-docs-sequence-step:1 + @libar-docs-sequence-module:pipeline-session Rule: MCP server starts via stdio transport and manages its own lifecycle **Invariant:** The MCP server communicates over stdio using JSON-RPC. It builds @@ -46,6 +49,10 @@ Feature: MCP Server Integration Any extraneous stdout output corrupts the JSON-RPC stream. Loading the pipeline during initialization ensures the first tool call is fast. + **Input:** SessionOptions -- input, features, baseDir, watch + + **Output:** PipelineSession -- dataset, api, registry, baseDir, sourceGlobs, buildTimeMs + **Verified by:** Server starts and responds to initialize, Server handles shutdown cleanly @@ -69,6 +76,8 @@ Feature: MCP Server Integration When the client sends an MCP initialize request Then the pipeline uses the explicit globs instead of config auto-detection + @libar-docs-sequence-step:2 + @libar-docs-sequence-module:tool-registry Rule: ProcessStateAPI methods and CLI subcommands are registered as MCP tools **Invariant:** Every CLI subcommand is registered as an MCP tool with a JSON @@ -79,8 +88,13 @@ Feature: MCP Server Integration description (for LLM tool selection), and JSON Schema for input validation. The "dp_" prefix prevents collisions in multi-server setups. + **Input:** PipelineSession -- dataset, api, registry + + **Output:** RegisteredTools -- 25 tools with dp_ prefix, Zod input schemas, handler functions + **Verified by:** All CLI subcommands appear as MCP tools, - Tool schemas validate input parameters + Tool schemas validate input parameters, + Pattern detail returns full metadata @acceptance-criteria @happy-path Scenario: All CLI subcommands appear as MCP tools @@ -96,23 +110,38 @@ Feature: MCP Server Integration When the client calls "dp_overview" Then the response contains the overview text with progress and phases - @acceptance-criteria @edge-case + @acceptance-criteria @edge-case @libar-docs-sequence-error Scenario: Tool call with missing required parameter returns error Given the MCP server is initialized When the client calls "dp_pattern" without the required "name" parameter Then the response is an MCP error indicating invalid params + @acceptance-criteria @happy-path + Scenario: Pattern detail returns full metadata + Given the MCP server is initialized + When the client calls "dp_pattern" for a pattern with rules and extracted shapes + Then the response contains the full pattern metadata payload + And the response includes deliverables, dependencies, business rules, and extracted shapes + + @libar-docs-sequence-step:3 + @libar-docs-sequence-module:pipeline-session Rule: MasterDataset is loaded once and reused across all tool invocations **Invariant:** The pipeline runs exactly once during server initialization. All subsequent tool calls read from in-memory MasterDataset. A manual rebuild can - be triggered via a "dp_rebuild" tool. + be triggered via a "dp_rebuild" tool, and overlapping rebuild requests coalesce + so the final in-memory session reflects the newest completed build. **Rationale:** The pipeline costs 2-5 seconds. Running it per tool call negates MCP benefits. Pre-computed views provide O(1) access ideal for a query server. + **Input:** ToolCallRequest -- tool name, arguments + + **Output:** ToolCallResult -- content, isError + **Verified by:** Multiple tool calls share one pipeline build, - Rebuild refreshes the dataset + Rebuild refreshes the dataset, + Concurrent rebuild requests coalesce @acceptance-criteria @happy-path Scenario: Multiple tool calls share one pipeline build @@ -128,13 +157,22 @@ Feature: MCP Server Integration Then the pipeline runs again And subsequent tool calls use the new dataset + @acceptance-criteria @happy-path + Scenario: Concurrent rebuild requests coalesce + Given the MCP server is running with a loaded dataset + When two rebuild requests arrive before the first rebuild completes + Then the server serializes the rebuild work + And the final in-memory session is the newest rebuilt dataset + @acceptance-criteria @edge-case Scenario: Concurrent reads during rebuild use previous dataset Given a rebuild is in progress When a tool call arrives for "dp_status" Then the call uses the previous dataset - And the response metadata indicates rebuild in progress + And the call completes successfully with the previous data + @libar-docs-sequence-step:4 + @libar-docs-sequence-module:file-watcher Rule: Source file changes trigger automatic dataset rebuild with debouncing **Invariant:** When --watch is enabled, changes to source files trigger an @@ -145,6 +183,10 @@ Feature: MCP Server Integration Without auto-rebuild, agents must manually call dp_rebuild. Debouncing prevents redundant rebuilds during rapid-fire saves. + **Input:** FileChangeEvent -- filePath, eventType + + **Output:** PipelineSession -- rebuilt dataset with updated patterns + **Verified by:** File change triggers rebuild, Rapid changes are debounced @@ -161,25 +203,34 @@ Feature: MCP Server Integration When 5 files are modified within 200ms Then the pipeline rebuilds exactly once after the debounce window - @acceptance-criteria @edge-case + @acceptance-criteria @edge-case @libar-docs-sequence-error Scenario: Rebuild failure during watch does not crash server Given the MCP server is running with --watch enabled When a source file change introduces a parse error Then the server continues using the previous valid dataset - And an MCP notification indicates rebuild failure + And the rebuild failure is logged to stderr + @libar-docs-sequence-step:5 + @libar-docs-sequence-module:mcp-server Rule: MCP server is configurable via standard client configuration **Invariant:** The server works with .mcp.json (Claude Code), claude_desktop_config.json (Claude Desktop), and any MCP client. It accepts --input, --features, --base-dir - args and auto-detects delivery-process.config.ts. + args, auto-detects delivery-process.config.ts, and reports the package version + accurately through the CLI. **Rationale:** MCP clients discover servers through configuration files. The server must work with sensible defaults (config auto-detection) while supporting explicit overrides for monorepo setups. + **Input:** CLIArgs -- input, features, baseDir, watch, help, version + + **Output:** McpServerOptions -- parsed options merged with config defaults + **Verified by:** Default config auto-detection, - Server works when started via npx + Config without explicit sources falls back to conventional globs, + Server works when started via npx, + Version flag reports package version @acceptance-criteria @happy-path Scenario: Default config auto-detection @@ -188,6 +239,13 @@ Feature: MCP Server Integration Then it loads globs from the config file And the pipeline builds successfully + @acceptance-criteria @happy-path + Scenario: Config without explicit sources falls back to conventional globs + Given a project with delivery-process.config.ts but no explicit sources + When the MCP server is started without explicit arguments + Then it falls back to the conventional source globs + And the pipeline builds successfully + @acceptance-criteria @happy-path Scenario: Server works when started via npx Given the package is installed @@ -195,7 +253,13 @@ Feature: MCP Server Integration Then the server process starts and awaits MCP initialize And no extraneous output appears on stdout - @acceptance-criteria @edge-case + @acceptance-criteria @happy-path + Scenario: Version flag reports package version + Given the package is installed + When running "npx @libar-dev/delivery-process dp-mcp-server --version" + Then the output contains the current package version + + @acceptance-criteria @edge-case @libar-docs-sequence-error Scenario: No config file and no explicit globs Given a directory without delivery-process.config.ts When the MCP server is started without arguments diff --git a/delivery-process/stubs/mcp-server-integration/file-watcher.ts b/delivery-process/stubs/mcp-server-integration/file-watcher.ts new file mode 100644 index 00000000..a345404d --- /dev/null +++ b/delivery-process/stubs/mcp-server-integration/file-watcher.ts @@ -0,0 +1,55 @@ +/** + * @libar-docs + * @libar-docs-status completed + * @libar-docs-implements MCPServerIntegration + * @libar-docs-uses MCPPipelineSession + * @libar-docs-used-by MCPServerImpl + * @libar-docs-target src/mcp/file-watcher.ts + * @libar-docs-since DS-MCP + * + * ## McpFileWatcher — Debounced Source File Watcher + * + * Watches TypeScript and Gherkin source files for changes, triggering + * debounced pipeline rebuilds. Uses chokidar v5 with 500ms default debounce. + * + * ### Design Decisions + * + * DD-1: Chokidar v5 with EventEmitter cast - chokidar v5 uses typed + * EventEmitter which requires Node 22+ @types/node. We cast to + * plain EventEmitter for Node 20 compatibility. + * + * DD-2: 500ms debounce window - balances responsiveness (developer sees + * updated data quickly) with efficiency (avoids rebuilding per keystroke + * during rapid editing). Configurable via debounceMs option. + * + * DD-3: File type filtering - only .ts and .feature file changes trigger + * rebuilds. Changes to .md, .json, or other files are silently ignored + * since they don't affect the MasterDataset. + * + * DD-4: Rebuild failure isolation - if a rebuild fails (e.g., parse error + * in modified file), the watcher logs the error and keeps the previous + * valid dataset. The server never crashes from a file-watch rebuild failure. + * + * DD-5: Synchronous start() - watch() from chokidar returns immediately + * (no async setup needed). The method is synchronous to satisfy the + * require-await lint rule. + * + * ### When to Use + * + * - When starting the MCP server with --watch flag + * - When implementing auto-rebuild on source changes + */ + +export interface FileWatcherOptions { + readonly globs: readonly string[]; + readonly baseDir: string; + readonly debounceMs?: number | undefined; + readonly sessionManager: import('./pipeline-session.js').PipelineSessionManager; + readonly log: (message: string) => void; +} + +export declare class McpFileWatcher { + constructor(options: FileWatcherOptions); + start(): void; + stop(): Promise; +} diff --git a/delivery-process/stubs/mcp-server-integration/pipeline-session.ts b/delivery-process/stubs/mcp-server-integration/pipeline-session.ts new file mode 100644 index 00000000..75feb3fa --- /dev/null +++ b/delivery-process/stubs/mcp-server-integration/pipeline-session.ts @@ -0,0 +1,75 @@ +/** + * @libar-docs + * @libar-docs-status completed + * @libar-docs-implements MCPServerIntegration + * @libar-docs-uses PipelineFactory, ProcessStateAPI, ConfigLoader + * @libar-docs-used-by MCPServerImpl, MCPToolRegistry, MCPFileWatcher + * @libar-docs-target src/mcp/pipeline-session.ts + * @libar-docs-since DS-MCP + * + * ## PipelineSessionManager — In-Memory MasterDataset Lifecycle + * + * Manages the persistent MasterDataset that all MCP tool calls read from. + * Loads config via auto-detection or explicit globs, builds the pipeline once, + * and provides atomic rebuild with concurrent-read safety. + * + * ### Architecture + * + * ``` + * SessionOptions + * | + * v + * PipelineSessionManager.initialize() + * |-- loadConfig() / applyProjectSourceDefaults() + * |-- buildMasterDataset() + * |-- createProcessStateAPI() + * v + * PipelineSession { dataset, api, registry, baseDir, sourceGlobs, buildTimeMs } + * ``` + * + * ### Design Decisions + * + * DD-1: Atomic dataset swap on rebuild - during rebuild, tool calls read from + * the previous PipelineSession. The new session replaces it atomically after + * a successful build. Failed rebuilds are logged, previous dataset stays active. + * + * DD-2: Config auto-detection mirrors the CLI - uses the same + * applyProjectSourceDefaults() from config-loader, then falls back to + * filesystem-based detection (delivery-process.config.ts presence, + * delivery-process/specs/ and delivery-process/stubs/ directories). + * + * DD-3: No caching layer - the CLI uses dataset-cache.ts for inter-process + * caching, but the MCP server keeps the dataset in-process memory. Caching + * would add complexity with no benefit for a long-lived server process. + * + * ### When to Use + * + * - When the MCP server needs to load or reload the pipeline + * - When implementing rebuild triggers (manual or file-watch) + */ + +export interface SessionOptions { + readonly input?: readonly string[] | undefined; + readonly features?: readonly string[] | undefined; + readonly baseDir?: string | undefined; + readonly watch?: boolean | undefined; +} + +export interface PipelineSession { + readonly dataset: import('../../src/generators/pipeline/index.js').RuntimeMasterDataset; + readonly api: import('../../src/api/process-state.js').ProcessStateAPI; + readonly registry: import('../../src/validation-schemas/tag-registry.js').TagRegistry; + readonly baseDir: string; + readonly sourceGlobs: { + readonly input: readonly string[]; + readonly features: readonly string[]; + }; + readonly buildTimeMs: number; +} + +export declare class PipelineSessionManager { + initialize(options?: SessionOptions): Promise; + rebuild(): Promise; + getSession(): PipelineSession; + isRebuilding(): boolean; +} diff --git a/delivery-process/stubs/mcp-server-integration/server.ts b/delivery-process/stubs/mcp-server-integration/server.ts new file mode 100644 index 00000000..24d37e5a --- /dev/null +++ b/delivery-process/stubs/mcp-server-integration/server.ts @@ -0,0 +1,63 @@ +/** + * @libar-docs + * @libar-docs-status completed + * @libar-docs-implements MCPServerIntegration + * @libar-docs-uses MCPPipelineSession, MCPToolRegistry, MCPFileWatcher + * @libar-docs-used-by MCPServerBin + * @libar-docs-target src/mcp/server.ts + * @libar-docs-since DS-MCP + * + * ## MCPServer — Entry Point and Lifecycle Manager + * + * Main entry point for the delivery-process MCP server. Wires together + * the pipeline session, tool registry, file watcher, and stdio transport. + * + * ### Lifecycle (5 phases) + * + * 1. Parse CLI args (--input, --features, --base-dir, --watch) + * 2. Initialize PipelineSessionManager (loads config, builds pipeline) + * 3. Create McpServer and register all tools + * 4. Optionally start file watcher + * 5. Connect via StdioServerTransport (enters JSON-RPC loop) + * + * ### Design Decisions + * + * DD-1: Stdout isolation via console.log redirect - MCP protocol uses JSON-RPC + * over stdout exclusively. console.log is redirected to console.error at module + * load time (before any imports). All application logging uses console.error + * via a log() helper that prefixes messages with [dp-mcp]. + * + * DD-2: Graceful shutdown with cleanup - SIGINT/SIGTERM handlers stop the file + * watcher, close the McpServer, then exit. Prevents dangling watchers and ensures + * the server process terminates cleanly when Claude Code closes the connection. + * + * DD-3: CLI arg parsing without commander.js - matches the project convention + * (manual arg parsing with for loop + switch). Supports --input/-i, --features/-f, + * --base-dir/-b, --watch/-w, --help/-h, --version/-v. + * + * DD-4: Config auto-detection with override - explicit --input/--features override + * config file detection. When no args are provided, the server auto-detects + * delivery-process.config.ts using the same applyProjectSourceDefaults() as CLI. + * + * DD-5: Pipeline failure at startup is fatal - if the pipeline fails during + * initialization (bad config, scan errors), the server exits with code 1. + * Pipeline failures during file-watch rebuilds are non-fatal (previous dataset stays). + * + * ### When to Use + * + * - When starting the MCP server from the CLI bin entry + * - When programmatically creating an MCP server instance + */ + +export interface McpServerOptions { + readonly input?: readonly string[] | undefined; + readonly features?: readonly string[] | undefined; + readonly baseDir?: string | undefined; + readonly watch?: boolean | undefined; + readonly version?: string | undefined; +} + +export declare function startMcpServer( + argv?: string[], + options?: McpServerOptions, +): Promise; diff --git a/delivery-process/stubs/mcp-server-integration/tool-registry.ts b/delivery-process/stubs/mcp-server-integration/tool-registry.ts new file mode 100644 index 00000000..c737b70d --- /dev/null +++ b/delivery-process/stubs/mcp-server-integration/tool-registry.ts @@ -0,0 +1,60 @@ +/** + * @libar-docs + * @libar-docs-status completed + * @libar-docs-implements MCPServerIntegration + * @libar-docs-uses ProcessStateAPI, MCPPipelineSession + * @libar-docs-used-by MCPServerImpl + * @libar-docs-target src/mcp/tool-registry.ts + * @libar-docs-since DS-MCP + * + * ## MCPToolRegistry — Tool Definitions with Zod Schemas + * + * Defines 25 MCP tools mapping to ProcessStateAPI methods and CLI subcommands. + * Each tool has a dp_ prefix, a Zod input schema for parameter validation, + * and a handler that delegates to existing API functions. + * + * ### Tool Categories (5 groups) + * + * | Category | Count | Output Format | Tools | + * |----------|-------|---------------|-------| + * | Session-aware | 6 | Formatted text | overview, context, files, dep_tree, scope_validate, handoff | + * | Data query | 9 | JSON | status, pattern, list, search, rules, tags, sources, stubs, decisions | + * | Architecture | 6 | JSON | arch_context, arch_layer, arch_neighborhood, arch_blocking, arch_dangling, arch_coverage | + * | Utility | 1 | JSON | unannotated | + * | Server mgmt | 3 | Text/JSON | rebuild, config, help | + * + * ### Design Decisions + * + * DD-1: Text output for session-aware tools - context, overview, scope-validate, + * handoff, files, dep-tree return formatted text identical to CLI output. This is + * what Claude Code expects for direct consumption. JSON would require the LLM to + * parse structure it already understands as text. + * + * DD-2: JSON output for data tools - pattern, list, status, stubs, rules return + * JSON for structured querying. Claude Code can extract specific fields. + * + * DD-3: Synchronous handlers where possible - the MCP SDK ToolCallback type + * accepts both sync and async returns. Handlers that don't need await are + * synchronous to avoid require-await lint violations. + * + * DD-4: No output pipeline - the CLI's output-pipeline.ts (namesOnly, count, + * fields modifiers) is replaced by dedicated tool parameters. Each tool has + * explicit namesOnly/count parameters instead of generic modifier flags. + * + * DD-5: dp_ prefix per spec invariant - avoids collision with other MCP servers + * in multi-server Claude Code setups. Matches the spec's Rule 2 invariant. + * + * ### When to Use + * + * - When registering tools on the McpServer instance + * - When adding a new tool to the MCP server + * - When understanding the mapping from CLI subcommands to MCP tools + */ + +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { PipelineSessionManager } from './pipeline-session.js'; + +export declare function registerAllTools( + server: McpServer, + sessionManager: PipelineSessionManager, +): void; diff --git a/docs/MCP-SETUP.md b/docs/MCP-SETUP.md new file mode 100644 index 00000000..343cd82e --- /dev/null +++ b/docs/MCP-SETUP.md @@ -0,0 +1,140 @@ +# MCP Server Setup + +> delivery-process MCP server exposes ProcessStateAPI as native Claude Code tools with sub-millisecond dispatch. + +## Quick Start + +### Claude Code (`.mcp.json`) + +Add to your project's `.mcp.json`: + +```json +{ + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": ["dp-mcp-server"], + "cwd": "${workspaceFolder}" + } + } +} +``` + +### Claude Desktop (`claude_desktop_config.json`) + +```json +{ + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": ["dp-mcp-server"], + "cwd": "/path/to/your/project" + } + } +} +``` + +### With File Watching + +Auto-rebuild the dataset when source files change: + +```json +{ + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": ["dp-mcp-server", "--watch"], + "cwd": "${workspaceFolder}" + } + } +} +``` + +### With Explicit Globs (Monorepo) + +Override config auto-detection for monorepo setups: + +```json +{ + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": [ + "dp-mcp-server", + "--input", + "packages/core/src/**/*.ts", + "--features", + "packages/core/specs/**/*.feature", + "--base-dir", + "packages/core" + ] + } + } +} +``` + +## How It Works + +The MCP server: + +1. **Loads the pipeline once** — config detection, scanning, extraction, transformation (~1-2s) +2. **Keeps MasterDataset in memory** — all subsequent queries are O(1) lookups +3. **Exposes 25 tools** — each wrapping a ProcessStateAPI method or CLI subcommand +4. **Optionally watches files** — auto-rebuilds on source changes (500ms debounce) + +## Available Tools + +| Tool | Description | +| ---------------------- | ---------------------------------------------------- | +| `dp_overview` | Project health summary (start here) | +| `dp_context` | Session-aware context bundle for a pattern | +| `dp_pattern` | Full pattern metadata | +| `dp_list` | List patterns with filters (status, phase, category) | +| `dp_search` | Fuzzy search patterns by name | +| `dp_status` | Status counts and completion percentage | +| `dp_files` | File reading list for a pattern | +| `dp_dep_tree` | Dependency chain with status | +| `dp_scope_validate` | Pre-flight check for implementation | +| `dp_handoff` | Session-end state for continuity | +| `dp_rules` | Business rules and invariants | +| `dp_tags` | Tag usage report | +| `dp_sources` | Source file inventory | +| `dp_stubs` | Design stubs with resolution status | +| `dp_decisions` | Design decisions from stubs | +| `dp_arch_context` | Bounded contexts with members | +| `dp_arch_layer` | Architecture layers with members | +| `dp_arch_neighborhood` | Pattern uses/used-by/peers | +| `dp_arch_blocking` | Patterns blocked by dependencies | +| `dp_arch_dangling` | Broken pattern references | +| `dp_arch_coverage` | Annotation coverage analysis | +| `dp_unannotated` | Files missing @libar-docs | +| `dp_rebuild` | Force dataset rebuild | +| `dp_config` | Show current configuration | +| `dp_help` | List all tools | + +## CLI Options + +```text +dp-mcp-server [options] + + -i, --input TypeScript source globs (repeatable) + -f, --features Gherkin feature globs (repeatable) + -b, --base-dir

Base directory (default: cwd) + -w, --watch Watch source files for changes + -h, --help Show help + -v, --version Show version +``` + +## Troubleshooting + +### Server fails to start + +Check that `delivery-process.config.ts` exists in your project root, or provide explicit `--input` and `--features` globs. + +### Tools return stale data + +Call `dp_rebuild` to force a dataset refresh, or start the server with `--watch` for automatic rebuilds. + +### Config not found in monorepo + +Use `--base-dir` to point to the package root and `--input`/`--features` for explicit glob patterns. diff --git a/package.json b/package.json index 0bb2dc42..0dd852b0 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,11 @@ "types": "./dist/validation/index.d.ts", "import": "./dist/validation/index.js" }, - "./package.json": "./package.json" + "./package.json": "./package.json", + "./mcp": { + "types": "./dist/mcp/index.d.ts", + "import": "./dist/mcp/index.js" + } }, "bin": { "generate-docs": "./dist/cli/generate-docs.js", @@ -65,7 +69,8 @@ "generate-tag-taxonomy": "./dist/cli/generate-tag-taxonomy.js", "validate-patterns": "./dist/cli/validate-patterns.js", "process-api": "./dist/cli/process-api.js", - "lint-steps": "./dist/cli/lint-steps.js" + "lint-steps": "./dist/cli/lint-steps.js", + "dp-mcp-server": "./dist/cli/mcp-server.js" }, "scripts": { "build": "tsc", @@ -181,7 +186,9 @@ "dependencies": { "@cucumber/gherkin": "^29.0.0", "@cucumber/messages": "^25.0.1", + "@modelcontextprotocol/sdk": "^1.27.1", "@typescript-eslint/typescript-estree": "^8.18.0", + "chokidar": "^5.0.0", "glob": "^10.3.10", "zod": "^4.1.11" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afe30857..84d39ae6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,15 @@ importers: '@cucumber/messages': specifier: ^25.0.1 version: 25.0.1 + '@modelcontextprotocol/sdk': + specifier: ^1.27.1 + version: 1.27.1(zod@4.3.6) '@typescript-eslint/typescript-estree': specifier: ^8.18.0 version: 8.53.1(typescript@5.9.3) + chokidar: + specifier: ^5.0.0 + version: 5.0.0 glob: specifier: ^10.3.10 version: 10.5.0 @@ -434,6 +440,12 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -484,6 +496,16 @@ packages: version: 0.1.0 hasBin: true + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -737,6 +759,10 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -747,9 +773,20 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-escapes@7.2.0: resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} @@ -784,6 +821,10 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -798,10 +839,22 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -822,6 +875,10 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -853,6 +910,26 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -873,9 +950,20 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -885,13 +973,29 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -902,6 +1006,9 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -957,13 +1064,35 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -977,6 +1106,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -997,6 +1129,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1012,15 +1148,34 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} @@ -1040,18 +1195,42 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hono@4.12.8: + resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} + engines: {node: '>=16.9.0'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1068,6 +1247,17 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1088,6 +1278,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1110,6 +1303,9 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jose@6.2.1: + resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -1120,6 +1316,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1166,6 +1368,18 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1174,6 +1388,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -1215,6 +1437,25 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -1241,6 +1482,10 @@ packages: parsecurrency@1.1.1: resolution: {integrity: sha512-IAw/8PSFgiko70KfZGv63rbEXhmVu+zpb42PvEtgHAm83Mze3eQJHWV1ZoOhPnrYeOyufvv0GS6hZDuQOdBH4Q==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -1256,6 +1501,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -1279,6 +1527,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -1292,16 +1544,40 @@ packages: engines: {node: '>=14'} hasBin: true + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1325,14 +1601,32 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1341,6 +1635,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1359,6 +1669,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -1428,6 +1742,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -1446,6 +1764,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript-eslint@8.53.1: resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1461,6 +1783,10 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1468,6 +1794,10 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1555,6 +1885,9 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} @@ -1564,6 +1897,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -1801,6 +2139,10 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@hono/node-server@1.19.11(hono@4.12.8)': + dependencies: + hono: 4.12.8 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -1845,6 +2187,28 @@ snapshots: '@libar-dev/modular-claude-md@https://codeload.github.com/libar-dev/modular-claude-md/tar.gz/3a37c573ae8611f1e0e92c00f565bb0ab45e1263': {} + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.8) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.8 + jose: 6.2.1 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2100,12 +2464,21 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 acorn@8.15.0: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -2113,6 +2486,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-escapes@7.2.0: dependencies: environment: 1.1.0 @@ -2135,6 +2515,20 @@ snapshots: balanced-match@4.0.4: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -2152,8 +2546,20 @@ snapshots: dependencies: fill-range: 7.1.1 + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} callsites@4.2.0: {} @@ -2173,6 +2579,10 @@ snapshots: check-error@2.1.3: {} + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + class-transformer@0.5.1: {} cli-cursor@5.0.0: @@ -2198,6 +2608,19 @@ snapshots: concat-map@0.0.1: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2212,18 +2635,38 @@ snapshots: deep-is@0.1.4: {} + depd@2.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + environment@1.1.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -2279,6 +2722,8 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} eslint-config-prettier@10.1.8(eslint@9.39.2): @@ -2355,10 +2800,56 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + expect-type@1.3.0: {} + express-rate-limit@8.3.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -2373,6 +2864,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -2389,6 +2882,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -2406,11 +2910,35 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -2434,12 +2962,34 @@ snapshots: globals@14.0.0: {} + gopd@1.2.0: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hono@4.12.8: {} + html-escaper@2.0.2: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + husky@9.1.7: {} + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -2451,6 +3001,12 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -2465,6 +3021,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -2494,6 +3052,8 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jose@6.2.1: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -2502,6 +3062,10 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} keyv@4.5.4: @@ -2564,6 +3128,12 @@ snapshots: dependencies: semver: 7.7.3 + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -2571,6 +3141,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-function@5.0.1: {} minimatch@10.1.1: @@ -2601,6 +3177,20 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -2630,6 +3220,8 @@ snapshots: parsecurrency@1.1.1: {} + parseurl@1.3.3: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -2641,6 +3233,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + pathe@1.1.2: {} pathval@2.0.1: {} @@ -2653,6 +3247,8 @@ snapshots: pidtree@0.6.0: {} + pkce-challenge@5.0.1: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -2663,12 +3259,34 @@ snapshots: prettier@3.8.1: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + readdirp@5.0.0: {} + reflect-metadata@0.2.2: {} + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -2713,18 +3331,85 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.56.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + safer-buffer@2.1.2: {} + semver@7.7.3: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -2738,6 +3423,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} string-argv@0.3.2: {} @@ -2804,6 +3491,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -2824,6 +3513,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typescript-eslint@8.53.1(eslint@9.39.2)(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) @@ -2839,12 +3534,16 @@ snapshots: undici-types@5.26.5: {} + unpipe@1.0.0: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 uuid@9.0.1: {} + vary@1.1.2: {} + vite-node@2.1.9(@types/node@20.10.0): dependencies: cac: 6.7.14 @@ -2936,8 +3635,14 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + yaml@2.8.2: {} yocto-queue@0.1.0: {} + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@4.3.6: {} diff --git a/src/api/fuzzy-match.ts b/src/api/fuzzy-match.ts index a767b0d1..f19b8fda 100644 --- a/src/api/fuzzy-match.ts +++ b/src/api/fuzzy-match.ts @@ -101,9 +101,11 @@ function scoreMatch( return { score: 1.0, matchType: 'exact' }; } - // Tier 2: Prefix match + // Tier 2: Prefix match — score increases with query coverage of the name if (nameLower.startsWith(queryLower)) { - return { score: 0.9, matchType: 'prefix' }; + const coverage = queryLower.length / nameLower.length; + const score = 0.9 + coverage * 0.09; + return { score: Math.min(score, 0.99), matchType: 'prefix' }; } // Tier 3: Substring match @@ -129,11 +131,11 @@ function scoreMatch( * * Scoring tiers (all case-insensitive): * 1. Exact match: score = 1.0 - * 2. Prefix match: score = 0.9 + * 2. Prefix match: score = 0.9 + (queryLen/nameLen) * 0.09 (shorter names rank higher) * 3. Substring match: score = 0.7 * 4. Levenshtein distance <= 3: score = 1 - (distance / max(len1, len2)) * - * Results are sorted by score descending, limited to maxResults. + * Results are sorted by score descending, with shorter names as tie-breaker. * * @param query - Search query string * @param patternNames - All available pattern names to search @@ -158,7 +160,14 @@ export function fuzzyMatchPatterns( } } - matches.sort((a, b) => b.score - a.score); + matches.sort((a, b) => { + if (b.score !== a.score) return b.score - a.score; + // Tie-breaker: shorter name = more specific match + if (a.patternName.length !== b.patternName.length) + return a.patternName.length - b.patternName.length; + // Final tie-breaker: lexical ordering for deterministic results + return a.patternName.localeCompare(b.patternName); + }); return matches.slice(0, maxResults); } diff --git a/src/api/scope-validator.ts b/src/api/scope-validator.ts index 9f1f0c7d..043586f8 100644 --- a/src/api/scope-validator.ts +++ b/src/api/scope-validator.ts @@ -253,6 +253,16 @@ export function checkFsmAllowsTransition( }; } + // Pattern is already active — no transition needed for implement session + if (status === 'active') { + return { + id: 'fsm-allows-transition', + label: 'FSM allows transition', + severity: 'PASS', + detail: 'Already active — no transition needed', + }; + } + const isValid = api.isValidTransition(status, 'active'); if (isValid) { diff --git a/src/cli/mcp-server.ts b/src/cli/mcp-server.ts new file mode 100644 index 00000000..35376e16 --- /dev/null +++ b/src/cli/mcp-server.ts @@ -0,0 +1,61 @@ +#!/usr/bin/env node +/** + * @libar-docs + * @libar-docs-core + * @libar-docs-pattern MCPServerBin + * @libar-docs-status active + * @libar-docs-arch-role infrastructure + * @libar-docs-arch-context cli + * @libar-docs-arch-layer infrastructure + * @libar-docs-uses MCPServerImpl + * @libar-docs-implements MCPServerIntegration + * + * ## MCP Server CLI Entry Point + * + * Handles stdout isolation, CLI arg parsing, and process lifecycle. + * Delegates to startMcpServer() for MCP server setup. + * + * Stdout isolation MUST happen before any MCP module loads — the dynamic + * import below ensures the console.log redirect is active first. + */ + +// Module marker — enables top-level await in TypeScript. +export {}; + +// Redirect console.log to stderr BEFORE loading MCP modules. +// MCP uses JSON-RPC over stdout — stray console.log corrupts the transport. +console.log = console.error; + +// Dynamic import ensures the redirect is active before server module loads. +const { parseCliArgs, startMcpServer } = await import('../mcp/server.js'); + +const parsed = parseCliArgs(process.argv.slice(2)); + +switch (parsed.type) { + case 'help': + console.error(parsed.text); + process.exit(0); + break; + case 'version': + console.error(parsed.text); + process.exit(0); + break; + case 'error': + console.error(`[dp-mcp] Error: ${parsed.message}`); + process.exit(1); + break; + case 'options': + try { + await startMcpServer({ + ...(parsed.options.input !== undefined ? { input: parsed.options.input } : {}), + ...(parsed.options.features !== undefined ? { features: parsed.options.features } : {}), + ...(parsed.options.baseDir !== undefined ? { baseDir: parsed.options.baseDir } : {}), + ...(parsed.options.watch !== undefined ? { watch: parsed.options.watch } : {}), + ...(parsed.options.version !== undefined ? { version: parsed.options.version } : {}), + }); + } catch (error: unknown) { + console.error(`Fatal error: ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } + break; +} diff --git a/src/config/config-loader.ts b/src/config/config-loader.ts index 8633bfdb..2dcc61ff 100644 --- a/src/config/config-loader.ts +++ b/src/config/config-loader.ts @@ -269,7 +269,7 @@ export type ProjectConfigLoadResult = * Only fills in sources not already provided by CLI flags. * * @param config - Mutable config object with baseDir, input, and features arrays - * @returns true if a non-default project config was found and applied + * @returns true if project config sources were applied to at least one empty source list */ export async function applyProjectSourceDefaults(config: { readonly baseDir: string; @@ -286,13 +286,17 @@ export async function applyProjectSourceDefaults(config: { } const resolved = result.value; + let applied = false; + if (config.input.length === 0 && resolved.project.sources.typescript.length > 0) { config.input.push(...resolved.project.sources.typescript); + applied = true; } if (config.features.length === 0 && resolved.project.sources.features.length > 0) { config.features.push(...resolved.project.sources.features); + applied = true; } - return true; + return applied; } export async function loadProjectConfig(baseDir: string): Promise { diff --git a/src/mcp/file-watcher.ts b/src/mcp/file-watcher.ts new file mode 100644 index 00000000..6cbe2b90 --- /dev/null +++ b/src/mcp/file-watcher.ts @@ -0,0 +1,127 @@ +/** + * @libar-docs + * @libar-docs-core + * @libar-docs-pattern MCPFileWatcher + * @libar-docs-status active + * @libar-docs-arch-role infrastructure + * @libar-docs-arch-context api + * @libar-docs-arch-layer infrastructure + * @libar-docs-implements MCPServerIntegration + * + * ## MCP File Watcher + * + * Watches source file globs and triggers debounced pipeline rebuilds. + * When a TypeScript or Gherkin file changes, the MasterDataset is rebuilt + * so subsequent tool calls reflect the updated annotations. + * + * ### When to Use + * + * - When starting the MCP server with --watch flag + * - When implementing auto-rebuild on source changes + */ + +import type { EventEmitter } from 'events'; +import { watch } from 'chokidar'; +import type { PipelineSessionManager } from './pipeline-session.js'; + +// ============================================================================= +// Types +// ============================================================================= + +export interface FileWatcherOptions { + readonly globs: readonly string[]; + readonly baseDir: string; + readonly debounceMs?: number; + readonly sessionManager: PipelineSessionManager; + readonly log: (message: string) => void; +} + +/** + * Returns true if the file type should trigger a pipeline rebuild. + * Only TypeScript (.ts) and Gherkin (.feature) files are watched. + */ +export function isWatchedFileType(filePath: string): boolean { + return filePath.endsWith('.ts') || filePath.endsWith('.feature'); +} + +// ============================================================================= +// File Watcher +// ============================================================================= + +export class McpFileWatcher { + private watcher: ReturnType | null = null; + private debounceTimer: ReturnType | null = null; + private readonly options: Required> & { + debounceMs: number; + }; + + constructor(options: FileWatcherOptions) { + this.options = { + ...options, + debounceMs: options.debounceMs ?? 500, + }; + } + + start(): void { + this.watcher = watch([...this.options.globs], { + cwd: this.options.baseDir, + ignoreInitial: true, + ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**'], + }); + + // chokidar v5 uses typed EventEmitter which requires Node 22+ @types/node. + // Use EventEmitter.on() via cast to maintain Node 20 compatibility. + const emitter = this.watcher as unknown as EventEmitter; + emitter.on('change', (filePath: string) => this.onFileChange(filePath)); + emitter.on('add', (filePath: string) => this.onFileChange(filePath)); + emitter.on('unlink', (filePath: string) => this.onFileChange(filePath)); + emitter.on('error', (error: Error) => { + this.options.log(`File watcher error: ${error.message}`); + }); + + this.options.log( + `File watcher started (${this.options.globs.length} glob patterns, ${this.options.debounceMs}ms debounce)` + ); + } + + async stop(): Promise { + if (this.debounceTimer !== null) { + clearTimeout(this.debounceTimer); + this.debounceTimer = null; + } + if (this.watcher !== null) { + await this.watcher.close(); + this.watcher = null; + this.options.log('File watcher stopped'); + } + } + + private onFileChange(filePath: string): void { + if (!isWatchedFileType(filePath)) { + return; + } + + if (this.debounceTimer !== null) { + clearTimeout(this.debounceTimer); + } + + this.debounceTimer = setTimeout(() => { + this.debounceTimer = null; + void this.triggerRebuild(filePath); + }, this.options.debounceMs); + } + + private async triggerRebuild(triggerFile: string): Promise { + this.options.log(`Source changed: ${triggerFile} — rebuilding dataset...`); + try { + const session = await this.options.sessionManager.rebuild(); + this.options.log( + `Dataset rebuilt in ${session.buildTimeMs}ms (${session.dataset.patterns.length} patterns)` + ); + } catch (error) { + this.options.log( + `Rebuild failed: ${error instanceof Error ? error.message : String(error)}. Keeping previous dataset.` + ); + } + } +} diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 00000000..a692700b --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,29 @@ +/** + * @libar-docs + * @libar-docs-core + * @libar-docs-pattern MCPModule + * @libar-docs-status active + * @libar-docs-arch-role infrastructure + * @libar-docs-arch-context api + * @libar-docs-arch-layer application + * @libar-docs-uses MCPServerImpl, MCPPipelineSession, MCPFileWatcher, MCPToolRegistry + * + * ## MCP Module Exports + * + * Public API for the MCP server module. + */ + +export { + startMcpServer, + parseCliArgs, + type McpServerOptions, + type ParsedOptions, + type ParseCliResult, +} from './server.js'; +export { + PipelineSessionManager, + type PipelineSession, + type SessionOptions, +} from './pipeline-session.js'; +export { McpFileWatcher, isWatchedFileType, type FileWatcherOptions } from './file-watcher.js'; +export { registerAllTools } from './tool-registry.js'; diff --git a/src/mcp/pipeline-session.ts b/src/mcp/pipeline-session.ts new file mode 100644 index 00000000..023ab53c --- /dev/null +++ b/src/mcp/pipeline-session.ts @@ -0,0 +1,211 @@ +/** + * @libar-docs + * @libar-docs-core + * @libar-docs-pattern MCPPipelineSession + * @libar-docs-status active + * @libar-docs-arch-role service + * @libar-docs-arch-context api + * @libar-docs-arch-layer application + * @libar-docs-uses PipelineFactory, ProcessStateAPI, ConfigLoader + * @libar-docs-implements MCPServerIntegration + * + * ## MCP Pipeline Session Manager + * + * Manages the in-memory MasterDataset lifecycle for the MCP server. + * Loads config, builds the pipeline once, and provides atomic rebuild. + * + * ### When to Use + * + * - When the MCP server needs a persistent ProcessStateAPI instance + * - When rebuilding the dataset after source file changes + */ + +import * as fs from 'node:fs'; +import * as path from 'path'; +import { + buildMasterDataset, + type PipelineResult, + type RuntimeMasterDataset, +} from '../generators/pipeline/index.js'; +import { createProcessStateAPI } from '../api/process-state.js'; +import type { ProcessStateAPI } from '../api/process-state.js'; +import type { TagRegistry } from '../validation-schemas/tag-registry.js'; +import { applyProjectSourceDefaults } from '../config/config-loader.js'; + +// ============================================================================= +// Types +// ============================================================================= + +export interface SessionOptions { + readonly input?: readonly string[] | undefined; + readonly features?: readonly string[] | undefined; + readonly baseDir?: string | undefined; + readonly watch?: boolean | undefined; +} + +export interface PipelineSession { + readonly dataset: RuntimeMasterDataset; + readonly api: ProcessStateAPI; + readonly registry: TagRegistry; + readonly baseDir: string; + readonly sourceGlobs: { readonly input: readonly string[]; readonly features: readonly string[] }; + readonly buildTimeMs: number; +} + +// ============================================================================= +// Pipeline Session Manager +// ============================================================================= + +export class PipelineSessionManager { + private session: PipelineSession | null = null; + private rebuildPromise: Promise | null = null; + private pendingRebuild = false; + + async initialize(options: SessionOptions = {}): Promise { + const baseDir = path.resolve(options.baseDir ?? process.cwd()); + + // Resolve source globs: explicit args override config auto-detection + const input: string[] = options.input ? [...options.input] : []; + const features: string[] = options.features ? [...options.features] : []; + + if (input.length === 0 || features.length === 0) { + const applied = await applyProjectSourceDefaults({ baseDir, input, features }); + if (!applied) { + // Fall back to convention-based detection + this.applyFallbackDefaults({ baseDir, input, features }); + } + } + + if (input.length === 0) { + throw new Error( + 'No TypeScript source globs found. Provide --input or create delivery-process.config.ts' + ); + } + + const session = await this.buildSession(baseDir, input, features); + this.session = session; + return session; + } + + async rebuild(): Promise { + if (this.session === null) { + throw new Error('Cannot rebuild: session not initialized'); + } + + if (this.rebuildPromise !== null) { + this.pendingRebuild = true; + return this.rebuildPromise; + } + + this.rebuildPromise = this.runRebuildLoop(); + try { + return await this.rebuildPromise; + } finally { + this.pendingRebuild = false; + this.rebuildPromise = null; + } + } + + getSession(): PipelineSession { + if (this.session === null) { + throw new Error('Session not initialized. Call initialize() first.'); + } + return this.session; + } + + isRebuilding(): boolean { + return this.rebuildPromise !== null; + } + + // --------------------------------------------------------------------------- + // Private + // --------------------------------------------------------------------------- + + private async runRebuildLoop(): Promise { + if (this.session === null) { + throw new Error('Cannot rebuild: session not initialized'); + } + + let latestSession = this.session; + + for (;;) { + this.pendingRebuild = false; + + const newSession = await this.buildSession( + latestSession.baseDir, + [...latestSession.sourceGlobs.input], + [...latestSession.sourceGlobs.features] + ); + this.session = newSession; + latestSession = newSession; + + // Another caller may set pendingRebuild while buildSession() is awaiting. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!this.pendingRebuild) { + return latestSession; + } + } + } + + private async buildSession( + baseDir: string, + input: readonly string[], + features: readonly string[] + ): Promise { + const startMs = Date.now(); + + const result = await buildMasterDataset({ + input, + features, + baseDir, + mergeConflictStrategy: 'fatal', + }); + + if (!result.ok) { + throw new Error(`Pipeline error [${result.error.step}]: ${result.error.message}`); + } + + const pipelineResult: PipelineResult = result.value; + const dataset = pipelineResult.dataset; + const api = createProcessStateAPI(dataset); + const buildTimeMs = Date.now() - startMs; + + return { + dataset, + api, + registry: dataset.tagRegistry, + baseDir, + sourceGlobs: { input, features }, + buildTimeMs, + }; + } + + private applyFallbackDefaults(config: { + baseDir: string; + input: string[]; + features: string[]; + }): void { + if (config.input.length === 0) { + const tsConfigPath = path.join(config.baseDir, 'delivery-process.config.ts'); + const jsConfigPath = path.join(config.baseDir, 'delivery-process.config.js'); + if (fs.existsSync(tsConfigPath) || fs.existsSync(jsConfigPath)) { + config.input.push('src/**/*.ts'); + const stubsDir = path.join(config.baseDir, 'delivery-process', 'stubs'); + if (fs.existsSync(stubsDir)) { + config.input.push('delivery-process/stubs/**/*.ts'); + } + } + } + + if (config.features.length === 0) { + const specsDir = path.join(config.baseDir, 'delivery-process', 'specs'); + if (fs.existsSync(specsDir)) { + config.features.push('delivery-process/specs/*.feature'); + } + const releasesDir = path.join(config.baseDir, 'delivery-process', 'releases'); + if (fs.existsSync(releasesDir)) { + config.features.push('delivery-process/releases/*.feature'); + } + } + } +} diff --git a/src/mcp/server.ts b/src/mcp/server.ts new file mode 100644 index 00000000..e0249356 --- /dev/null +++ b/src/mcp/server.ts @@ -0,0 +1,237 @@ +/** + * @libar-docs + * @libar-docs-core + * @libar-docs-pattern MCPServerImpl + * @libar-docs-status active + * @libar-docs-arch-role service + * @libar-docs-arch-context api + * @libar-docs-arch-layer application + * @libar-docs-uses MCPPipelineSession, MCPToolRegistry, MCPFileWatcher + * @libar-docs-implements MCPServerIntegration + * + * ## MCP Server Entry Point + * + * Main entry point for the delivery-process MCP server. + * Initializes the pipeline, registers tools, and connects via stdio transport. + * + * Stdout isolation (console.log → stderr redirect) is handled by the CLI + * entry point (`src/cli/mcp-server.ts`) before this module loads. + * + * ### When to Use + * + * - When starting the MCP server from the CLI bin entry + * - When programmatically creating an MCP server instance + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { getPackageVersion } from '../cli/version.js'; +import { PipelineSessionManager, type SessionOptions } from './pipeline-session.js'; +import { registerAllTools } from './tool-registry.js'; +import { McpFileWatcher } from './file-watcher.js'; + +// ============================================================================= +// Types +// ============================================================================= + +export interface McpServerOptions extends SessionOptions { + readonly watch?: boolean; + readonly version?: string; +} + +export interface ParsedOptions { + input?: readonly string[] | undefined; + features?: readonly string[] | undefined; + baseDir?: string | undefined; + watch?: boolean | undefined; + version?: string | undefined; +} + +export type ParseCliResult = + | { readonly type: 'options'; readonly options: ParsedOptions } + | { readonly type: 'help'; readonly text: string } + | { readonly type: 'version'; readonly text: string } + | { readonly type: 'error'; readonly message: string }; + +// ============================================================================= +// Server +// ============================================================================= + +function log(message: string): void { + console.error(`[dp-mcp] ${message}`); +} + +const DEFAULT_MCP_SERVER_VERSION = getPackageVersion(); + +export async function startMcpServer(options: McpServerOptions = {}): Promise { + // Initialize pipeline session + const sessionManager = new PipelineSessionManager(); + + log('Initializing pipeline...'); + const session = await sessionManager.initialize({ + ...(options.input !== undefined ? { input: options.input } : {}), + ...(options.features !== undefined ? { features: options.features } : {}), + ...(options.baseDir !== undefined ? { baseDir: options.baseDir } : {}), + }); + log(`Pipeline built in ${session.buildTimeMs}ms (${session.dataset.patterns.length} patterns)`); + + // Create MCP server + const version = options.version ?? DEFAULT_MCP_SERVER_VERSION; + const server = new McpServer( + { name: 'delivery-process', version }, + { capabilities: { logging: {} } } + ); + + // Register all tools + registerAllTools(server, sessionManager); + log('Tools registered'); + + // Start file watcher if requested + let fileWatcher: McpFileWatcher | null = null; + if (options.watch === true) { + fileWatcher = new McpFileWatcher({ + globs: [...session.sourceGlobs.input, ...session.sourceGlobs.features], + baseDir: session.baseDir, + sessionManager, + log, + }); + fileWatcher.start(); + } + + // Handle shutdown — idempotent guard prevents double-close races + // when multiple signals/EOF fire concurrently. + let cleanupPromise: Promise | null = null; + const cleanup = (): Promise => { + if (cleanupPromise !== null) return cleanupPromise; + cleanupPromise = (async (): Promise => { + log('Shutting down...'); + if (fileWatcher !== null) { + await fileWatcher.stop(); + } + await server.close(); + log('Shutdown complete'); + })(); + return cleanupPromise; + }; + + process.once('SIGINT', () => { + void cleanup().then(() => process.exit(0)); + }); + process.once('SIGTERM', () => { + void cleanup().then(() => process.exit(0)); + }); + + // Connect via stdio transport + const transport = new StdioServerTransport(); + await server.connect(transport); + log('MCP server running on stdio'); + + // Tear down file watcher when client disconnects (stdin EOF). + // Without this, chokidar holds the event loop open indefinitely. + process.stdin.once('end', () => { + log('Client disconnected (stdin closed)'); + void cleanup().then(() => process.exit(0)); + }); +} + +// ============================================================================= +// CLI Arg Parser +// ============================================================================= + +export function parseCliArgs(argv: string[], defaults: McpServerOptions = {}): ParseCliResult { + const result: ParsedOptions = { ...defaults }; + const input: string[] = []; + const features: string[] = []; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + + switch (arg) { + case '--input': + case '-i': { + const val = argv[++i]; + if (val === undefined || val.startsWith('-')) { + return { type: 'error', message: '--input requires a glob value' }; + } + input.push(val); + break; + } + case '--features': + case '-f': { + const val = argv[++i]; + if (val === undefined || val.startsWith('-')) { + return { type: 'error', message: '--features requires a glob value' }; + } + features.push(val); + break; + } + case '--base-dir': + case '-b': { + const val = argv[++i]; + if (val === undefined || val.startsWith('-')) { + return { type: 'error', message: '--base-dir requires a directory path' }; + } + result.baseDir = val; + break; + } + case '--watch': + case '-w': { + result.watch = true; + break; + } + case '--version': + case '-v': { + return { + type: 'version', + text: 'dp-mcp-server v' + (defaults.version ?? DEFAULT_MCP_SERVER_VERSION), + }; + } + case '--help': + case '-h': { + return { type: 'help', text: HELP_TEXT }; + } + case '--': { + // Skip pnpm separator + break; + } + default: { + if (typeof arg === 'string' && arg.startsWith('-')) { + return { type: 'error', message: `Unknown flag: "${arg}"` }; + } + break; + } + } + } + + if (input.length > 0) result.input = input; + if (features.length > 0) result.features = features; + + return { type: 'options', options: result }; +} + +const HELP_TEXT = ` +dp-mcp-server — MCP server for delivery-process + +Usage: dp-mcp-server [options] + +Options: + -i, --input TypeScript source globs (repeatable) + -f, --features Gherkin feature globs (repeatable) + -b, --base-dir Base directory (default: cwd) + -w, --watch Watch source files for changes + -h, --help Show this help + -v, --version Show version + +The server auto-detects delivery-process.config.ts if no explicit +globs are provided. Configure in Claude Code via .mcp.json: + + { + "mcpServers": { + "delivery-process": { + "command": "npx", + "args": ["dp-mcp-server"], + "cwd": "/path/to/project" + } + } + } +`.trim(); diff --git a/src/mcp/tool-registry.ts b/src/mcp/tool-registry.ts new file mode 100644 index 00000000..8692f070 --- /dev/null +++ b/src/mcp/tool-registry.ts @@ -0,0 +1,725 @@ +/** + * @libar-docs + * @libar-docs-core + * @libar-docs-pattern MCPToolRegistry + * @libar-docs-status active + * @libar-docs-arch-role service + * @libar-docs-arch-context api + * @libar-docs-arch-layer application + * @libar-docs-uses ProcessStateAPI, MCPPipelineSession + * @libar-docs-implements MCPServerIntegration + * + * ## MCP Tool Registry + * + * Defines all MCP tools with Zod input schemas and handler functions. + * Each tool wraps a ProcessStateAPI method or CLI subcommand. + * Tool names use "dp_" prefix to avoid collisions with other MCP servers. + * + * ### When to Use + * + * - When registering tools on the McpServer instance + * - When adding a new tool to the MCP server + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { PipelineSession, PipelineSessionManager } from './pipeline-session.js'; +import { + assembleContext, + buildDepTree, + buildFileReadingList, + buildOverview, + isValidSessionType, + type SessionType, +} from '../api/context-assembler.js'; +import { + formatContextBundle, + formatDepTree, + formatFileReadingList, + formatOverview, +} from '../api/context-formatter.js'; +import { + computeNeighborhood, + aggregateTagUsage, + buildSourceInventory, +} from '../api/arch-queries.js'; +import { analyzeCoverage, findUnannotatedFiles } from '../api/coverage-analyzer.js'; +import { validateScope, formatScopeValidation, type ScopeType } from '../api/scope-validator.js'; +import { + generateHandoff, + formatHandoff, + type HandoffSessionType, +} from '../api/handoff-generator.js'; +import { queryBusinessRules } from '../api/rules-query.js'; +import { findStubPatterns, resolveStubs, groupStubsByPattern } from '../api/stub-resolver.js'; +import { fuzzyMatchPatterns } from '../api/fuzzy-match.js'; +import { allPatternNames, getPatternName } from '../api/pattern-helpers.js'; +import { summarizePatterns } from '../api/summarize.js'; + +// ============================================================================= +// Types +// ============================================================================= + +type TextContent = { type: 'text'; text: string }; +type ToolResult = { content: TextContent[]; isError?: boolean }; + +function textResult(text: string): ToolResult { + return { content: [{ type: 'text', text }] }; +} + +function jsonResult(data: unknown): ToolResult { + return textResult(JSON.stringify(data, null, 2)); +} + +function errorResult(message: string): ToolResult { + return { content: [{ type: 'text', text: message }], isError: true }; +} + +// ============================================================================= +// Tool Registration +// ============================================================================= + +export function registerAllTools(server: McpServer, sessionManager: PipelineSessionManager): void { + const getSession = (): PipelineSession => sessionManager.getSession(); + + // Wrap handlers so thrown exceptions become MCP error payloads + // instead of propagating as transport-level errors. + function safeHandler( + fn: (args: T) => ToolResult | Promise + ): (args: T) => ToolResult | Promise { + return (args: T): ToolResult | Promise => { + try { + const result = fn(args); + if (result instanceof Promise) { + return result.catch( + (error: unknown): ToolResult => + errorResult(error instanceof Error ? error.message : String(error)) + ); + } + return result; + } catch (error: unknown) { + return errorResult(error instanceof Error ? error.message : String(error)); + } + }; + } + + // --------------------------------------------------------------------------- + // Session-aware tools (text output — matches CLI text output) + // --------------------------------------------------------------------------- + + server.registerTool( + 'dp_overview', + { + title: 'Project Overview', + description: + 'Get project health summary: progress percentage, active phases, blocking chains, and data API commands. Start here to understand project state.', + inputSchema: z.object({}), + }, + safeHandler(() => { + const s = getSession(); + const overview = buildOverview(s.dataset); + return textResult(formatOverview(overview)); + }) + ); + + server.registerTool( + 'dp_context', + { + title: 'Pattern Context', + description: + 'Get curated context bundle for a pattern, tailored to a session type (planning/design/implement). Returns spec, dependencies, deliverables, and FSM state.', + inputSchema: z.object({ + name: z.string().describe('Pattern name'), + session: z + .enum(['planning', 'design', 'implement']) + .optional() + .describe('Session type (planning, design, implement)'), + }), + }, + safeHandler(({ name, session }) => { + const s = getSession(); + const validated = session ?? ''; + const sessionType: SessionType = isValidSessionType(validated) ? validated : 'implement'; + const bundle = assembleContext(s.dataset, s.api, { + patterns: [name], + sessionType, + baseDir: s.baseDir, + }); + return textResult(formatContextBundle(bundle)); + }) + ); + + server.registerTool( + 'dp_files', + { + title: 'File Reading List', + description: + 'Get ordered file list for a pattern with implementation paths and roles. Use before reading code.', + inputSchema: z.object({ + name: z.string().describe('Pattern name'), + }), + }, + safeHandler(({ name }) => { + const s = getSession(); + const fileList = buildFileReadingList(s.dataset, name, true); + return textResult(formatFileReadingList(fileList)); + }) + ); + + server.registerTool( + 'dp_dep_tree', + { + title: 'Dependency Tree', + description: + 'Get dependency chain for a pattern showing status of each dependency. Useful for understanding what must be completed first.', + inputSchema: z.object({ + name: z.string().describe('Pattern name'), + maxDepth: z.number().optional().describe('Maximum depth (default: 10)'), + }), + }, + safeHandler(({ name, maxDepth }) => { + const s = getSession(); + const tree = buildDepTree(s.dataset, { + pattern: name, + maxDepth: maxDepth ?? 10, + includeImplementationDeps: false, + }); + return textResult(formatDepTree(tree)); + }) + ); + + server.registerTool( + 'dp_scope_validate', + { + title: 'Scope Validation', + description: + 'Pre-flight check before starting work on a pattern. Validates FSM state, dependencies, deliverables, and design decisions.', + inputSchema: z.object({ + name: z.string().describe('Pattern name'), + session: z.enum(['implement', 'design']).describe('Session type (implement or design)'), + }), + }, + safeHandler(({ name, session }) => { + const s = getSession(); + const result = validateScope(s.api, s.dataset, { + patternName: name, + scopeType: session as ScopeType, + baseDir: s.baseDir, + }); + return textResult(formatScopeValidation(result)); + }) + ); + + server.registerTool( + 'dp_handoff', + { + title: 'Session Handoff', + description: + 'Generate session-end state for continuity. Captures progress, remaining work, and next steps.', + inputSchema: z.object({ + name: z.string().describe('Pattern name'), + session: z + .enum(['planning', 'design', 'implement', 'review']) + .optional() + .describe('Session type'), + }), + }, + safeHandler(({ name, session }) => { + const s = getSession(); + const handoff = generateHandoff(s.api, s.dataset, { + patternName: name, + sessionType: (session ?? 'implement') as HandoffSessionType, + }); + return textResult(formatHandoff(handoff)); + }) + ); + + // --------------------------------------------------------------------------- + // Data query tools (JSON output) + // --------------------------------------------------------------------------- + + server.registerTool( + 'dp_status', + { + title: 'Status Counts', + description: + 'Get pattern counts by status (completed, active, roadmap, deferred) and completion percentage.', + inputSchema: z.object({}), + }, + safeHandler(() => { + const s = getSession(); + return jsonResult({ + counts: s.api.getStatusCounts(), + distribution: s.api.getStatusDistribution(), + }); + }) + ); + + server.registerTool( + 'dp_pattern', + { + title: 'Pattern Detail', + description: + 'Get full metadata for a single pattern: status, phase, deliverables, relationships, business rules, and extracted shapes.', + inputSchema: z.object({ + name: z.string().describe('Pattern name (case-insensitive)'), + }), + }, + safeHandler(({ name }) => { + const s = getSession(); + const pattern = s.api.getPattern(name); + if (pattern === undefined) { + return errorResult(`Pattern "${name}" not found.`); + } + const canonicalName = getPatternName(pattern); + return jsonResult({ + ...pattern, + deliverables: s.api.getPatternDeliverables(canonicalName), + dependencies: s.api.getPatternDependencies(canonicalName) ?? null, + relationships: s.api.getPatternRelationships(canonicalName) ?? null, + }); + }) + ); + + server.registerTool( + 'dp_list', + { + title: 'List Patterns', + description: + 'List patterns with optional filters. Supports filtering by status, phase, and category. Use namesOnly for compact output.', + inputSchema: z.object({ + status: z + .enum(['completed', 'active', 'roadmap', 'deferred']) + .optional() + .describe('Filter by status (completed, active, roadmap, deferred)'), + phase: z.number().optional().describe('Filter by phase number'), + category: z.string().optional().describe('Filter by category'), + namesOnly: z.boolean().optional().describe('Return only pattern names'), + count: z.boolean().optional().describe('Return only the count'), + }), + }, + safeHandler(({ status, phase, category, namesOnly, count }) => { + const s = getSession(); + let patterns = [...s.dataset.patterns]; + + if (status !== undefined) { + patterns = patterns.filter((p) => p.status === status); + } + if (phase !== undefined) { + patterns = patterns.filter((p) => p.phase === phase); + } + if (category !== undefined) { + patterns = patterns.filter((p) => p.category === category); + } + + if (count === true) { + return textResult(`${patterns.length} patterns`); + } + if (namesOnly === true) { + return textResult(patterns.map((p) => p.name).join('\n')); + } + return jsonResult(summarizePatterns(patterns)); + }) + ); + + server.registerTool( + 'dp_search', + { + title: 'Search Patterns', + description: + 'Fuzzy search for patterns by name. Returns ranked matches with similarity scores.', + inputSchema: z.object({ + query: z.string().describe('Search query string'), + }), + }, + safeHandler(({ query }) => { + const s = getSession(); + const names = allPatternNames(s.dataset); + const matches = fuzzyMatchPatterns(query, names); + return jsonResult(matches); + }) + ); + + server.registerTool( + 'dp_rules', + { + title: 'Business Rules', + description: + 'Query business rules and invariants extracted from Gherkin Rule: blocks. Filter by pattern name.', + inputSchema: z.object({ + pattern: z.string().optional().describe('Filter rules by pattern name'), + onlyInvariants: z + .boolean() + .optional() + .describe('Return only invariants (skip rationale/scenarios)'), + }), + }, + safeHandler(({ pattern, onlyInvariants }) => { + const s = getSession(); + const result = queryBusinessRules(s.dataset, { + productArea: null, + patternName: pattern ?? null, + onlyInvariants: onlyInvariants ?? false, + }); + + // Without pattern filter, return compact summary to avoid 800K+ response + if (pattern === undefined) { + return jsonResult({ + totalRules: result.totalRules, + totalInvariants: result.totalInvariants, + allRuleNames: result.allRuleNames, + productAreas: result.productAreas.map((pa) => ({ + productArea: pa.productArea, + ruleCount: pa.ruleCount, + invariantCount: pa.invariantCount, + })), + hint: 'Use the "pattern" parameter to get full rule details for a specific pattern.', + }); + } + return jsonResult(result); + }) + ); + + server.registerTool( + 'dp_tags', + { + title: 'Tag Usage Report', + description: 'Get tag inventory: counts per tag and value across all annotated sources.', + inputSchema: z.object({}), + }, + safeHandler(() => { + const s = getSession(); + return jsonResult(aggregateTagUsage(s.dataset)); + }) + ); + + server.registerTool( + 'dp_sources', + { + title: 'Source Inventory', + description: + 'Get file inventory by source type (TypeScript, Gherkin, stubs) with pattern counts.', + inputSchema: z.object({}), + }, + safeHandler(() => { + const s = getSession(); + return jsonResult(buildSourceInventory(s.dataset)); + }) + ); + + server.registerTool( + 'dp_stubs', + { + title: 'Design Stubs', + description: + 'Get design stub files grouped by pattern. Shows which stubs have implementations and which are unresolved.', + inputSchema: z.object({ + unresolved: z + .boolean() + .optional() + .describe('Show only unresolved stubs (no implementation yet)'), + }), + }, + safeHandler(({ unresolved }) => { + const s = getSession(); + const stubs = findStubPatterns(s.dataset); + const resolutions = resolveStubs(stubs, s.baseDir); + + if (unresolved === true) { + const unresolvedOnly = resolutions.filter((r) => !r.targetExists); + return jsonResult({ unresolvedCount: unresolvedOnly.length, stubs: unresolvedOnly }); + } + return jsonResult(groupStubsByPattern(resolutions)); + }) + ); + + server.registerTool( + 'dp_decisions', + { + title: 'Design Decisions', + description: 'Extract design decisions (AD-N / DD-N) from pattern descriptions.', + inputSchema: z.object({ + name: z.string().optional().describe('Pattern name (shows all if omitted)'), + }), + }, + safeHandler(({ name }) => { + const s = getSession(); + // Extract decisions from pattern descriptions (stub descriptions contain AD-N / DD-N entries) + const stubs = findStubPatterns(s.dataset); + const decisions: Array<{ pattern: string; id: string; description: string }> = []; + + for (const stub of stubs) { + const desc = stub.directive.description; + const regex = /((?:AD|DD)-\d+):\s*(.+?)(?:\n|$)/g; + let match = regex.exec(desc); + while (match !== null) { + const id = match[1] ?? ''; + const matchedDesc = match[2]?.trim() ?? ''; + decisions.push({ pattern: stub.name, id, description: matchedDesc }); + match = regex.exec(desc); + } + } + + if (name !== undefined) { + const filtered = decisions.filter((d) => d.pattern.toLowerCase() === name.toLowerCase()); + return jsonResult(filtered); + } + return jsonResult(decisions); + }) + ); + + // --------------------------------------------------------------------------- + // Architecture tools + // --------------------------------------------------------------------------- + + server.registerTool( + 'dp_arch_context', + { + title: 'Architecture Contexts', + description: + 'Get bounded contexts with member patterns. Optionally filter to a specific context name.', + inputSchema: z.object({ + name: z.string().optional().describe('Filter to a specific bounded context'), + }), + }, + safeHandler(({ name }) => { + const s = getSession(); + if (s.dataset.archIndex === undefined) { + return jsonResult([]); + } + const byContext = s.dataset.archIndex.byContext; + if (name !== undefined) { + const patterns = byContext[name] ?? []; + return jsonResult(summarizePatterns(patterns)); + } + const contexts = Object.entries(byContext).map(([context, patterns]) => ({ + context, + count: patterns.length, + patterns: patterns.map((p) => p.name), + })); + return jsonResult(contexts); + }) + ); + + server.registerTool( + 'dp_arch_layer', + { + title: 'Architecture Layers', + description: + 'Get architecture layers with member patterns. Optionally filter to a specific layer.', + inputSchema: z.object({ + name: z.string().optional().describe('Filter to a specific architecture layer'), + }), + }, + safeHandler(({ name }) => { + const s = getSession(); + if (s.dataset.archIndex === undefined) { + return jsonResult([]); + } + const byLayer = s.dataset.archIndex.byLayer; + if (name !== undefined) { + const patterns = byLayer[name] ?? []; + return jsonResult(summarizePatterns(patterns)); + } + const layers = Object.entries(byLayer).map(([layer, patterns]) => ({ + layer, + count: patterns.length, + patterns: patterns.map((p) => p.name), + })); + return jsonResult(layers); + }) + ); + + server.registerTool( + 'dp_arch_neighborhood', + { + title: 'Pattern Neighborhood', + description: + 'Get everything a pattern touches: uses, used-by, same-context peers, and dependency status.', + inputSchema: z.object({ + name: z.string().describe('Pattern name'), + }), + }, + safeHandler(({ name }) => { + const s = getSession(); + return jsonResult(computeNeighborhood(name, s.dataset)); + }) + ); + + server.registerTool( + 'dp_arch_blocking', + { + title: 'Blocked Patterns', + description: + 'Find patterns blocked by incomplete dependencies. Shows which dependencies must be completed first.', + inputSchema: z.object({}), + }, + safeHandler(() => { + const s = getSession(); + const overview = buildOverview(s.dataset); + return jsonResult(overview.blocking); + }) + ); + + server.registerTool( + 'dp_arch_dangling', + { + title: 'Dangling References', + description: 'Find broken references to nonexistent pattern names in relationship tags.', + inputSchema: z.object({}), + }, + safeHandler(() => { + const s = getSession(); + const allNames = new Set(s.dataset.patterns.map((p) => p.name)); + const dangling: Array<{ pattern: string; tag: string; target: string }> = []; + + for (const p of s.dataset.patterns) { + for (const dep of p.dependsOn ?? []) { + if (!allNames.has(dep)) + dangling.push({ pattern: p.name, tag: 'depends-on', target: dep }); + } + for (const u of p.uses ?? []) { + if (!allNames.has(u)) dangling.push({ pattern: p.name, tag: 'uses', target: u }); + } + for (const ub of p.usedBy ?? []) { + if (!allNames.has(ub)) dangling.push({ pattern: p.name, tag: 'used-by', target: ub }); + } + } + + return jsonResult(dangling); + }) + ); + + server.registerTool( + 'dp_arch_coverage', + { + title: 'Annotation Coverage', + description: + 'Analyze annotation coverage across source files. Returns coverage percentage, annotated/total counts, and unused taxonomy values.', + inputSchema: z.object({ + path: z.string().optional().describe('Filter to a specific directory path'), + }), + }, + safeHandler(async ({ path: pathFilter }) => { + const s = getSession(); + const globs = + pathFilter !== undefined + ? (() => { + // Preserve file extensions from configured globs instead of hardcoding *.ts + const extensions = new Set(); + for (const g of s.sourceGlobs.input) { + const m = /\*\.(\w+)$/.exec(g); + if (m?.[1] !== undefined) extensions.add(m[1]); + } + if (extensions.size === 0) extensions.add('ts'); + return [...extensions].map((ext) => `${pathFilter}/**/*.${ext}`); + })() + : [...s.sourceGlobs.input]; + const report = await analyzeCoverage(s.dataset, globs, s.baseDir, s.registry); + return jsonResult(report); + }) + ); + + server.registerTool( + 'dp_unannotated', + { + title: 'Unannotated Files', + description: + 'Find TypeScript files missing @libar-docs annotations. Optionally filter by directory.', + inputSchema: z.object({ + path: z.string().optional().describe('Filter to a specific directory path'), + }), + }, + safeHandler(async ({ path: pathFilter }) => { + const s = getSession(); + const unannotated = await findUnannotatedFiles( + [...s.sourceGlobs.input], + s.baseDir, + s.registry, + pathFilter + ); + return jsonResult(unannotated); + }) + ); + + // --------------------------------------------------------------------------- + // Server management tools + // --------------------------------------------------------------------------- + + server.registerTool( + 'dp_rebuild', + { + title: 'Rebuild Dataset', + description: + 'Force rebuild of the in-memory MasterDataset from current source files. Use after making changes to annotated sources.', + inputSchema: z.object({}), + }, + safeHandler(async () => { + const newSession = await sessionManager.rebuild(); + return textResult( + `Dataset rebuilt in ${newSession.buildTimeMs}ms. ${newSession.dataset.patterns.length} patterns loaded.` + ); + }) + ); + + server.registerTool( + 'dp_config', + { + title: 'Current Configuration', + description: + 'Show current project configuration: source globs, base directory, and build time.', + inputSchema: z.object({}), + }, + safeHandler(() => { + const s = getSession(); + return jsonResult({ + baseDir: s.baseDir, + sourceGlobs: s.sourceGlobs, + buildTimeMs: s.buildTimeMs, + patternCount: s.dataset.patterns.length, + phaseCount: s.dataset.phaseCount, + categoryCount: s.dataset.categoryCount, + }); + }) + ); + + server.registerTool( + 'dp_help', + { + title: 'MCP Tools Help', + description: + 'List all available MCP tools with descriptions. Use this to discover what queries are available.', + inputSchema: z.object({}), + }, + safeHandler(() => { + const tools = [ + 'dp_overview - Project health summary (start here)', + 'dp_context - Session-aware context bundle for a pattern', + 'dp_pattern - Full pattern metadata', + 'dp_list - List patterns with filters', + 'dp_search - Fuzzy search patterns', + 'dp_status - Status counts and completion %', + 'dp_files - File reading list for a pattern', + 'dp_dep_tree - Dependency chain with status', + 'dp_scope_validate - Pre-flight check for implementation', + 'dp_handoff - Session-end state for continuity', + 'dp_rules - Business rules and invariants', + 'dp_tags - Tag usage report', + 'dp_sources - Source file inventory', + 'dp_stubs - Design stubs with resolution status', + 'dp_decisions - Design decisions from stubs', + 'dp_arch_context - Bounded contexts with members', + 'dp_arch_layer - Architecture layers with members', + 'dp_arch_neighborhood - Pattern uses/used-by/peers', + 'dp_arch_blocking - Patterns blocked by dependencies', + 'dp_arch_dangling - Broken pattern references', + 'dp_arch_coverage - Annotation coverage analysis', + 'dp_unannotated - Files missing @libar-docs', + 'dp_rebuild - Force dataset rebuild', + 'dp_config - Show current configuration', + 'dp_help - This help text', + ]; + return textResult(`delivery-process MCP Server — Available Tools\n\n${tools.join('\n')}`); + }) + ); +} diff --git a/tests/features/api/output-shaping/fuzzy-match.feature b/tests/features/api/output-shaping/fuzzy-match.feature index 62453c8d..e9028065 100644 --- a/tests/features/api/output-shaping/fuzzy-match.feature +++ b/tests/features/api/output-shaping/fuzzy-match.feature @@ -10,7 +10,7 @@ Feature: Fuzzy Pattern Matching **Invariant:** Pattern matching must use a tiered scoring system: exact match (1.0) > prefix match (0.9) > substring match (0.7) > Levenshtein distance, with results sorted by score descending and case-insensitive matching. **Rationale:** Tiered scoring ensures the most intuitive match wins — an exact match should always rank above a substring match, preventing surprising suggestions for common pattern names. - **Verified by:** Exact match scores 1.0, Exact match is case-insensitive, Prefix match scores 0.9, Substring match scores 0.7, Levenshtein match for close typos, Results are sorted by score descending, Empty query matches all patterns as prefix, No candidate patterns returns no results + **Verified by:** Exact match scores 1.0, Exact match is case-insensitive, Prefix match scores above 0.9, Substring match scores 0.7, Levenshtein match for close typos, Results are sorted by score descending, Prefix matches rank shorter names higher, Prefix match scores reflect query coverage, Empty query matches all patterns as prefix, No candidate patterns returns no results @acceptance-criteria @happy-path Scenario: Exact match scores 1.0 @@ -25,10 +25,11 @@ Feature: Fuzzy Pattern Matching Then the top result is "OrderSaga" with score "1.0" and matchType "exact" @acceptance-criteria @happy-path - Scenario: Prefix match scores 0.9 + Scenario: Prefix match scores above 0.9 Given pattern names "AgentCommandInfrastructure", "EventStore" When I fuzzy match "AgentCommand" - Then the top result is "AgentCommandInfrastructure" with score "0.9" and matchType "prefix" + Then the top result is "AgentCommandInfrastructure" with matchType "prefix" + And the top result score is above "0.9" @acceptance-criteria @happy-path Scenario: Substring match scores 0.7 @@ -50,6 +51,18 @@ Feature: Fuzzy Pattern Matching Then the first result has score "1.0" And the second result has score at least "0.7" + @acceptance-criteria @happy-path + Scenario: Prefix matches rank shorter names higher + Given pattern names "MCPModule", "MCPServerIntegration", "MCPToolRegistry" + When I fuzzy match "MCP" + Then the first result is "MCPModule" + + @acceptance-criteria @happy-path + Scenario: Prefix match scores reflect query coverage + Given pattern names "MCPModule", "MCPServerIntegration" + When I fuzzy match "MCP" + Then the first result score is higher than the second result score + @edge-case Scenario: Empty query matches all patterns as prefix Given pattern names "OrderSaga", "EventStore" diff --git a/tests/features/api/session-support/scope-validator.feature b/tests/features/api/session-support/scope-validator.feature index 02a3132d..eddb8aee 100644 --- a/tests/features/api/session-support/scope-validator.feature +++ b/tests/features/api/session-support/scope-validator.feature @@ -16,7 +16,7 @@ Feature: Scope Validator - Pre-flight Session Readiness Checks **Invariant:** Implementation scope validation must check FSM transition validity, dependency completeness, PDR references, and deliverable presence, with strict mode promoting warnings to blockers. **Rationale:** Starting implementation without passing scope validation wastes an entire session — the validator catches all known blockers before any code is written. - **Verified by:** All implementation checks pass, Incomplete dependency blocks implementation, FSM transition from completed blocks implementation, Missing PDR references produce WARN, No deliverables blocks implementation, Strict mode promotes WARN to BLOCKED, Pattern not found throws error + **Verified by:** All implementation checks pass, Incomplete dependency blocks implementation, FSM transition from completed blocks implementation, Active pattern passes FSM check for implementation, Missing PDR references produce WARN, No deliverables blocks implementation, Strict mode promotes WARN to BLOCKED, Pattern not found throws error @acceptance-criteria @happy-path Scenario: All implementation checks pass @@ -39,6 +39,13 @@ Feature: Scope Validator - Pre-flight Session Readiness Checks Then the verdict is "blocked" And the FSM check shows BLOCKED + @acceptance-criteria @happy-path + Scenario: Active pattern passes FSM check for implementation + Given a pattern with active status and deliverables + When validating scope for implement session + Then the FSM check shows PASS + And the detail mentions already active + @acceptance-criteria @edge-case Scenario: Missing PDR references produce WARN Given a pattern with no stubs or PDR references diff --git a/tests/features/mcp/mcp-server.feature b/tests/features/mcp/mcp-server.feature new file mode 100644 index 00000000..0a460f43 --- /dev/null +++ b/tests/features/mcp/mcp-server.feature @@ -0,0 +1,241 @@ +@libar-docs +Feature: MCP Server Integration Tests + + Verifies the MCP-specific layer: tool registration, pipeline session + lifecycle, file watcher behavior, CLI argument parsing, and output + formatting. ProcessStateAPI correctness is tested separately. + + Background: Test infrastructure + Given a test MasterDataset is initialized for MCP + + Rule: Pipeline session manager loads once and supports atomic rebuild + + **Invariant:** The pipeline runs exactly once during initialization. + All subsequent calls read from in-memory MasterDataset. Rebuild + atomically replaces the session. + + **Rationale:** Verifies the core lifecycle contract of PipelineSessionManager + without requiring real file I/O. + + **Verified by:** Session initializes and contains dataset, + getSession throws before initialization, + Config without sources falls back to conventional globs, + Rebuild replaces session atomically, + getSession returns the previous session during rebuild, + Concurrent rebuild requests coalesce to the newest session, + isRebuilding flag lifecycle + + Scenario: Session initializes and contains dataset + Given a PipelineSessionManager initialized with test data + When getSession is called + Then the session contains a MasterDataset with patterns + And the session records build time in milliseconds + + Scenario: getSession throws before initialization + Given a new uninitialized PipelineSessionManager + When getSession is called without initialization + Then it throws an error containing "Session not initialized" + + Scenario: Config without sources falls back to conventional globs + Given a temp project with a config file but no sources and conventional directories + When the PipelineSessionManager initializes from that temp project + Then initialization succeeds using fallback source globs + And the session source globs include conventional TypeScript and feature paths + + Scenario: Rebuild replaces session atomically + Given a PipelineSessionManager initialized with test data + When rebuild is called + Then a new session is returned + And the new session has a different build time than the original + + Scenario: getSession returns the previous session during rebuild + Given a PipelineSessionManager initialized with test data + When rebuild is started without awaiting + Then getSession still returns the original session during rebuild + When the rebuild completes + And getSession returns the rebuilt session after completion + + Scenario: Concurrent rebuild requests coalesce to the newest session + Given a PipelineSessionManager initialized with test data + When two rebuild calls are started without awaiting + Then isRebuilding returns true while concurrent rebuilds are pending + When both rebuild calls complete + Then both rebuild calls resolve to the same latest session + And getSession returns that same latest session + + Scenario: isRebuilding flag lifecycle + Given a PipelineSessionManager initialized with test data + Then isRebuilding returns false before rebuild + When rebuild is started without awaiting + Then isRebuilding returns true during rebuild + When the rebuild completes + Then isRebuilding returns false after rebuild completes + + Rule: Tool registration creates correctly named tools with schemas + + **Invariant:** Every CLI subcommand is registered as an MCP tool with + dp_ prefix, non-empty description, and Zod input schema. + + **Rationale:** Verifies tool registration correctness via MockMcpServer + that records registerTool calls. + + **Verified by:** All tools registered with dp_ prefix, + Each tool has a non-empty description, + dp_overview returns formatted text, + dp_pattern returns error for unknown pattern, + dp_list filters apply cumulatively + + Scenario: All tools registered with dp_ prefix + Given an McpServer mock with registered tools + Then at least 25 tools are registered + And each tool name starts with "dp_" + + Scenario: Each tool has a non-empty description + Given an McpServer mock with registered tools + Then each registered tool has a non-empty description + + Scenario: dp_overview returns formatted text + Given an McpServer mock with registered tools + When the dp_overview handler is called + Then the result contains text content + And the result is not an error + + Scenario: dp_pattern returns error for unknown pattern + Given an McpServer mock with registered tools + When the dp_pattern handler is called with name "NonExistentPattern" + Then the result is an error + And the error message contains "not found" + + Scenario: dp_list filters apply cumulatively + Given a session with patterns of mixed status and phase + And an McpServer mock with registered tools using that session + When dp_list is called with status "active" and phase 46 + Then only patterns matching both status and phase are returned + + Rule: File watcher filters file types correctly + + **Invariant:** Only .ts and .feature files trigger rebuild. + Other file types are ignored. + + **Rationale:** Verifies the file type guard extracted as isWatchedFileType. + + **Verified by:** TypeScript files trigger rebuild, + Feature files trigger rebuild, + Non-watched file types are ignored + + Scenario: TypeScript files trigger rebuild + When checking if "src/mcp/server.ts" is a watched file type + Then the file is watched + + Scenario: Feature files trigger rebuild + When checking if "specs/mcp-server.feature" is a watched file type + Then the file is watched + + Scenario Outline: Non-watched file types are ignored + When checking if is a watched file type + Then the file is not watched + + Examples: + | file | + | package.json | + | README.md | + | styles.css | + + Rule: CLI argument parser handles all flag variants + + **Invariant:** The parser supports --input, --features, --base-dir, + --watch with short forms, handles -- separator, and reports version correctly. + + **Rationale:** Verifies the exported parseCliArgs function directly. + + **Verified by:** Input flag with long form, + Input flag with short form, + Features flag with long form, + Watch flag enables watching, + Base-dir flag sets directory, + Multiple input globs accumulate, + Double-dash separator is skipped, + Version flag returns package version + + Scenario Outline: CLI flags are parsed correctly + When parseCliArgs is called with "" + Then the parsed result has "