Skip to content

feat(mcp): add graph query tools and enrich project summary metadata#2638

Merged
davydkov merged 1 commit into
likec4:mainfrom
ckeller42:feat/improve-mcp-data
Feb 20, 2026
Merged

feat(mcp): add graph query tools and enrich project summary metadata#2638
davydkov merged 1 commit into
likec4:mainfrom
ckeller42:feat/improve-mcp-data

Conversation

@ckeller42

@ckeller42 ckeller42 commented Feb 18, 2026

Copy link
Copy Markdown
Collaborator

Add new MCP tools for advanced graph traversal, filtering, and analysis:

New Query Tools

  • query-graph: Navigate element hierarchy (ancestors, descendants, siblings, children, parent) and single-hop relationships (incomers, outgoers)
  • query-incomers-graph: Recursive BFS graph of all upstream dependencies/producers
  • query-outgoers-graph: Recursive BFS graph of all downstream consumers/dependents
  • query-by-metadata: Search elements by metadata key-value pairs with exact/contains/exists matching
  • query-by-tags: Filter elements by tags with boolean logic (allOf, anyOf, noneOf)
  • query-by-tag-pattern: Search elements by tag patterns using prefix, contains, or suffix matching
  • find-relationship-paths: Discover multi-hop relationship chains between elements using bounded BFS
  • batch-read-elements: Read full details for multiple elements in a single request
  • subgraph-summary: Summarize descendants of an element with depth, metadata, and relationship counts
  • element-diff: Compare two elements and show differences in properties, tags, metadata, and relationships

Enhancements

  • Enhanced read-project-summary to include serialized project configuration
  • Extended project config schema (@likec4/config) to support optional metadata field

Testing

  • All tests passing (128 new MCP tool tests across 12 test files)
  • Comprehensive coverage for all new tools including edge cases, error handling, and multi-hop traversals

Changes

  • 32 files changed
  • ~6,500 insertions

Changeset

  • @likec4/language-server: minor
  • @likec4/mcp: minor
  • @likec4/config: minor

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added 10 new query tools for deep architecture analysis: graph traversal, relationship discovery, metadata and tag filtering, element comparison, and batch operations
    • Added metadata field to project configuration to store arbitrary project-level key-value data
    • Enhanced project summary endpoint to include serialized configuration
  • Documentation

    • Updated MCP documentation with newly available query tools and capabilities

@changeset-bot

changeset-bot Bot commented Feb 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0587b66

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
Name Type
@likec4/language-server Minor
@likec4/mcp Minor
@likec4/config Minor
@likec4/playground Minor
@likec4/language-services Minor
likec4 Minor
@likec4/vite-plugin Minor
likec4-vscode Minor
@likec4/docs-astro Minor
@likec4/style-preset Minor
@likec4/styles Minor
@likec4/core Minor
@likec4/diagram Minor
@likec4/generators Minor
@likec4/layouts Minor
@likec4/log Minor
@likec4/react Minor
@likec4/tsconfig Minor
@likec4/vscode-preview Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ckeller42 ckeller42 marked this pull request as draft February 18, 2026 15:33
@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch from fd93754 to ea99b33 Compare February 18, 2026 15:33
@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds many new MCP query and analysis tools (graph, incomers/outgoers, tag/metadata queries, tag-pattern search, path-finding), batch-read and element-diff utilities, project config metadata support and serialization, MCP registrations, documentation, and extensive unit/integration tests; read-project-summary now returns serialized project config.

Changes

Cohort / File(s) Summary
Config Schema & Docs
packages/config/src/schema.ts, packages/config/README.md, apps/docs/src/content/docs/dsl/Config/index.mdx
Added optional metadata record to project config schema and updated docs/examples to document metadata usage.
Changeset & Gitignore
.changeset/mcp-query-tools-and-project-config-metadata.md, .gitignore
Bumped package versions in changeset; added ignore entries for local tooling/cache and local test scripts.
Core Serialization & Traversal
packages/language-server/src/mcp/tools/_common.ts, packages/language-server/src/mcp/tools/_common.spec.ts
Introduced projectConfigSchema, SerializableProjectConfig, serializeConfig(), element serialization, includedInViews helper, graph traversal types and traverseGraph() plus extensive tests.
MCP Server Registration & Read Summary
packages/language-server/src/mcp/server/MCPServerFactory.ts, packages/language-server/src/mcp/tools/read-project-summary.ts, packages/mcp/README.md, apps/docs/src/content/docs/tooling/mcp.mdx
Registered multiple new tools in MCPServerFactory; read-project-summary output now includes serialized config; updated MCP tooling docs.
Graph Querying Tools
packages/language-server/src/mcp/tools/query-graph.ts, packages/language-server/src/mcp/tools/query-graph.spec.ts
Added query-graph tool supporting ancestors/descendants/siblings/children/parent/incomers/outgoers with result caps and tests.
Incomers / Outgoers Subgraphs
packages/language-server/src/mcp/tools/query-incomers-graph.ts, packages/language-server/src/mcp/tools/query-incomers-graph.spec.ts, packages/language-server/src/mcp/tools/query-outgoers-graph.ts, packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts
Added BFS-based query-incomers-graph and query-outgoers-graph with configurable depth/node limits, cycle-safe traversal, truncation flags, shaped outputs and tests.
Metadata & Tag Queries
packages/language-server/src/mcp/tools/query-by-metadata.ts, packages/language-server/src/mcp/tools/query-by-metadata.spec.ts, packages/language-server/src/mcp/tools/query-by-tags.ts, packages/language-server/src/mcp/tools/query-by-tags.spec.ts, packages/language-server/src/mcp/tools/query-by-tag-pattern.ts, packages/language-server/src/mcp/tools/query-by-tag-pattern.spec.ts
Added query-by-metadata (exact/contains/exists), query-by-tags (allOf/anyOf/noneOf), and query-by-tag-pattern (prefix/contains/suffix) with matched-value/tag reporting, limits, and tests.
Path Finding, Batch & Diff Tools
packages/language-server/src/mcp/tools/find-relationship-paths.ts, packages/language-server/src/mcp/tools/find-relationship-paths.spec.ts, packages/language-server/src/mcp/tools/batch-read-elements.ts, packages/language-server/src/mcp/tools/batch-read-elements.spec.ts, packages/language-server/src/mcp/tools/element-diff.ts, packages/language-server/src/mcp/tools/element-diff.spec.ts
Added find-relationship-paths (BFS path discovery with caps), batchReadElements (up to 50 ids), and element-diff (property/tags/metadata/relationship diffs) with implementations and tests.
Subgraph Summary
packages/language-server/src/mcp/tools/subgraph-summary.ts, packages/language-server/src/mcp/tools/subgraph-summary.spec.ts
Added subgraph-summary to summarize descendants (depth, counts, filtered metadata), BFS traversal, truncation signals, and tests.
Integration Tests
packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts
Added end-to-end integration tests exercising multiple new tools against a multi-file cloud-system model.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant MCP as MCP Server
  participant LS as LanguageServices
  participant Model as ProjectModel

  Client->>MCP: Request tool (e.g. "query-graph", args)
  MCP->>LS: Invoke tool handler (resolve project, validate input)
  LS->>Model: Load/resolve elements, traverse graph or query metadata
  Model-->>LS: Return nodes, neighbors, relationships, metadata
  LS-->>MCP: Package results (results[], truncated/config)
  MCP-->>Client: Respond with results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • davydkov

Poem

🐇 I hop through nodes and metadata glades,
I sniff for tags in branching arcades,
I serialize configs neat and small,
I trace the paths that link them all,
Hooray — new tools to fetch and parse the mall!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: adding graph query tools (feat) and enriching project summary with metadata.
Description check ✅ Passed The PR description is comprehensive with detailed sections for new tools, enhancements, testing, and changeset impact, but the checklist template is not filled out.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch from ea99b33 to 6629554 Compare February 18, 2026 15:34
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/language-server/src/mcp/tools/read-project-summary.ts (1)

98-105: ⚠️ Potential issue | 🟡 Minor

Minor inconsistency in example response: "name" vs "id" for views.

The example response (line 100) shows a "name" field for views, but the actual output schema (line 147) and implementation (line 194) use "id". This could confuse consumers reading the docs.

📝 Suggested fix in the description example
   "views": [
     {
-      "name": "system-overview",
+      "id": "system-overview",
       "title": "System Overview",
       "type": "element"
     }
   ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/read-project-summary.ts` around lines
98 - 105, The example JSON for the "views" array uses "name" but the actual
schema/implementation expect "id"; update the example in read-project-summary.ts
so each view object uses "id" (e.g., "id": "system-overview") instead of "name"
to match the schema and the implementation (the "views" example block should
mirror the schema/implementation's "id" field).
🧹 Nitpick comments (10)
packages/config/src/schema.ts (1)

82-84: Consider z.unknown() instead of z.any() for stricter type inference.

z.any() infers as any in TypeScript, which disables type checking on the values. Using z.record(z.string(), z.unknown()) would preserve runtime behavior while inferring Record<string, unknown>, keeping downstream consumers type-safe. As per coding guidelines: "Use TypeScript with explicit types in all TypeScript files; avoid using any".

♻️ Suggested change
-  metadata: z.record(z.string(), z.any())
+  metadata: z.record(z.string(), z.unknown())
     .optional()
     .meta({ description: 'Arbitrary metadata as key-value pairs for custom project information' }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/schema.ts` around lines 82 - 84, Replace z.any() with
z.unknown() in the metadata schema definition so the line using
z.record(z.string(), z.any()).optional().meta(...) becomes z.record(z.string(),
z.unknown()).optional().meta(...); this preserves runtime validation while
changing the inferred TypeScript type to Record<string, unknown> and improving
type-safety for consumers of the metadata field in the schema.ts metadata
definition.
packages/language-server/src/mcp/tools/_common.ts (1)

49-50: config: any and result: any bypass type checking.

The function parameter and intermediate variable both use any, disabling type safety throughout the function body. Consider typing config with a more specific type (e.g., the config type from @likec4/config) and result as Partial<SerializableProjectConfig>.

♻️ Suggested change
-export function serializeConfig(config: any): SerializableProjectConfig {
-  const result: any = {
+export function serializeConfig(config: Record<string, unknown>): SerializableProjectConfig {
+  const result: Partial<SerializableProjectConfig> = {
     name: config.name,
   }

Note: This would require minor type adjustments or assertions in the body (e.g., config.title as string), but provides compile-time safety.

As per coding guidelines: "Use TypeScript with explicit types in all TypeScript files; avoid using any".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/_common.ts` around lines 49 - 50, The
serializeConfig function currently uses loose any types; change the parameter
type from any to the concrete config interface exported by `@likec4/config` (e.g.,
ProjectConfig or the appropriate exported type) and change the intermediate
result variable to Partial<SerializableProjectConfig> instead of any; update
usages inside serializeConfig (e.g., config.title, config.routes) with narrow
type assertions or optional checks as needed to satisfy the compiler and return
a fully populated SerializableProjectConfig, keeping the function name
serializeConfig and the target SerializableProjectConfig type for locating the
change.
packages/language-server/src/mcp/tools/query-by-tags.ts (1)

109-110: Silent truncation at 50 results — consider adding a truncated indicator.

Unlike the graph tools (e.g., query-outgoers-graph) which return a truncated: boolean flag when results are capped, this tool silently stops at 50 with no indication to the caller that more matches exist. This could mislead an MCP consumer into believing only 50 elements match.

♻️ Suggested change

Add a truncated boolean to the output schema and set it when the limit is hit:

-  const results = []
-  const limit = 50
+  const results = []
+  const limit = 50
+  let truncated = false

Then before returning:

-  return { results }
+  return { results, truncated }

And set truncated = true when breaking out of either loop due to the limit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts` around lines 109 -
110, The function currently collects matches into the results array and caps at
const limit = 50 without telling callers; add a truncated boolean to the
function's output schema and to the returned object (e.g., { results, truncated
}), introduce and initialize let truncated = false, set truncated = true
immediately before breaking out of any loop when results.length >= limit, and
ensure the final return includes this truncated flag alongside results so
callers can detect silent truncation (reference the results and limit symbols in
this file).
packages/language-server/src/mcp/tools/query-outgoers-graph.ts (1)

141-142: queue.shift() is O(n) per call — consider using an index-based approach for larger graphs.

Array.shift() re-indexes the array on each call, making the BFS loop O(n²) in the worst case. With the 5000-node cap this is bounded, but a simple index pointer avoids the overhead entirely:

♻️ Suggested alternative
-  while (queue.length > 0) {
-    const { elementId, depth } = queue.shift()!
+  let queueIdx = 0
+  while (queueIdx < queue.length) {
+    const { elementId, depth } = queue[queueIdx++]!
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.ts` around lines
141 - 142, The BFS loop currently uses queue.shift(), which is O(n) per call;
replace the shift-based dequeue with an index-based pointer (e.g., maintain a
head/index and read queue[head++] while head < queue.length) so dequeuing
becomes O(1) and avoids re-indexing; update the loop in query-outgoers-graph.ts
where queue.shift() is used (the while (queue.length > 0) { const { elementId,
depth } = queue.shift()! } block) to use the head pointer and continue using
queue.push(...) for enqueues.
packages/language-server/src/mcp/tools/query-by-tags.spec.ts (1)

351-353: Consider an exact count assertion.

The model defines exactly two elements (tagged and untagged), both of which should match noneOf: ['deprecated']. Using toBeGreaterThanOrEqual(2) is valid but loose — toHaveLength(2) would be more precise and would catch unexpected extra results.

♻️ Suggested tightening
-      expect(results.length).toBeGreaterThanOrEqual(2)
+      expect(results).toHaveLength(2)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-tags.spec.ts` around lines
351 - 353, The test currently checks results.length with
toBeGreaterThanOrEqual(2), which is loose; change the assertion to
expect(results).toHaveLength(2) (or expect(results.length).toBe(2)) to enforce
the exact expected count for the array extracted from result.structuredContent
(see the variable results and the earlier
expect(result.structuredContent).toBeDefined() in the test for guidance).
packages/language-server/src/mcp/tools/query-incomers-graph.ts (1)

141-142: Consider replacing Array.shift() with a queue index for large graphs.

Array.shift() is O(n) because it re-indexes every element. With maxNodes up to 5000 and potentially many enqueued duplicates, this could become a bottleneck on large graphs. A simple index-pointer pattern avoids the overhead.

♻️ Suggested optimization
-  const queue: Array<{ elementId: string; depth: number }> = [{ elementId: args.elementId, depth: 0 }]
+  const queue: Array<{ elementId: string; depth: number }> = [{ elementId: args.elementId, depth: 0 }]
+  let queueIdx = 0

-  while (queue.length > 0) {
-    const { elementId, depth } = queue.shift()!
+  while (queueIdx < queue.length) {
+    const { elementId, depth } = queue[queueIdx++]!
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts` around lines
141 - 142, The loop currently uses Array.shift() (in the while that destructures
{ elementId, depth } from queue.shift()!) which is O(n) per pop; replace this
with an index-pointer pattern: introduce a numeric queueHead = 0, push items
onto the existing queue array as before, and change the loop to run while
(queueHead < queue.length) and read items with queue[queueHead++] instead of
shift(); keep the rest of the logic (enqueuing, depth checks, maxNodes)
unchanged and ensure any length checks use queue.length - queueHead if you need
the number of pending items.
packages/language-server/src/mcp/tools/find-relationship-paths.ts (1)

196-236: Duplicated PathStep construction.

The pathStep object construction at lines 200–210 and 226–236 is identical. Consider extracting a helper.

♻️ Sketch
+    const toPathStep = (rel: typeof currentElement.outgoing extends () => Iterable<infer R> ? R : never): PathStep => ({
+      source: rel.source.id,
+      target: rel.target.id,
+      relationship: {
+        kind: rel.kind,
+        title: rel.title,
+        description: rel.description?.text ?? null,
+        technology: rel.technology,
+        tags: [...rel.tags],
+      },
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts` around
lines 196 - 236, Extract the duplicated PathStep construction into a small
helper function (e.g., buildPathStep or makePathStep) that takes the
relationship object (rel) and returns the PathStep, then replace both inline
constructions (the ones used when nextId === target.id and when enqueueing for
exploration) with calls to this helper; update usages around foundPaths.push and
the queue push so they use the helper result while preserving source:
rel.source.id, target: rel.target.id and relationship fields (kind, title,
description.text, technology, tags).
packages/language-server/src/mcp/tools/query-by-metadata.ts (2)

151-195: Consider extracting the duplicated element-processing logic.

The iteration bodies for model elements (lines 152–171) and deployment elements (lines 174–195) are identical aside from the data source. A shared helper or chaining the iterables would reduce duplication.

♻️ Sketch
+  const processElements = (elements: Iterable<{ id: string; name: string; kind: string; title: string; tags: Iterable<string>; getMetadata(): Record<string, any>; views(): Iterable<any> }>) => {
+    for (const el of elements) {
+      if (results.length >= limit) break
+      const metadata = el.getMetadata()
+      if (args.key in metadata) {
+        const metadataValue = metadata[args.key]
+        if (metadataValue !== undefined && matches(metadataValue, args.value, matchMode)) {
+          results.push({
+            id: el.id,
+            name: el.name,
+            kind: el.kind,
+            title: el.title,
+            tags: [...el.tags],
+            metadata,
+            matchedValue: getMatchedValue(metadataValue, args.value, matchMode),
+            includedInViews: includedInViews(el.views()),
+          })
+        }
+      }
+    }
+  }
+
+  processElements(model.elements())
+  processElements(model.deployment.elements())
-  // Search through elements
-  for (const element of model.elements()) {
-    ...
-  }
-  // Search through deployment nodes (if not at limit yet)
-  if (results.length < limit) {
-    for (const deploymentElement of model.deployment.elements()) {
-      ...
-    }
-  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts` around lines 151
- 195, Extract the duplicated element-processing logic into a helper (e.g.,
processElement or processElements) and call it for both model.elements() and
model.deployment.elements(); the helper should accept an element (or an
iterable) plus shared state (results, limit, args, matchMode) and perform the
existing checks and push the same object shape using matches, getMatchedValue
and includedInViews; replace the two for-loops with calls that iterate the two
sources (model.elements() and model.deployment.elements()) and early-exit when
results.length >= limit to preserve current behavior.

110-129: Use the matchMode literal union type instead of string for the mode parameter.

Both matches (line 110) and getMatchedValue (line 132) accept mode: string, losing type safety. Use the inferred type from the schema or an explicit literal union.

♻️ Proposed fix
-  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: string): boolean => {
+  type MatchMode = 'exact' | 'contains' | 'exists'
+  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: MatchMode): boolean => {

Apply the same change to getMatchedValue at line 132.

As per coding guidelines, "Use TypeScript with explicit types in all TypeScript files; avoid using any".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts` around lines 110
- 129, The functions matches and getMatchedValue currently accept mode: string
which loses type safety; update both signatures to use the literal union type
for match modes (e.g. 'exists' | 'exact' | 'contains' or the existing MatchMode
type from your schema if available) so the compiler enforces allowed values, and
adjust any call sites if necessary to match the new type; ensure
imports/reference to the schema's MatchMode (or define the explicit union) is
added so both matches and getMatchedValue use the same precise type instead of
string.
packages/language-server/src/mcp/tools/query-graph.ts (1)

121-235: Consider extracting the repeated result-mapping to a helper.

Every switch case maps an element to the same { id, name, kind, title, tags, metadata, includedInViews } shape. A small helper would reduce ~100 lines of boilerplate.

♻️ Sketch
+  const toResult = (el: typeof element) => ({
+    id: el.id,
+    name: el.name,
+    kind: el.kind,
+    title: el.title,
+    tags: [...el.tags],
+    metadata: el.getMetadata(),
+    includedInViews: includedInViews(el.views()),
+  })
+
   switch (args.queryType) {
     case 'ancestors': {
       for (const ancestor of element.ancestors()) {
-        results.push({
-          id: ancestor.id,
-          name: ancestor.name,
-          kind: ancestor.kind,
-          title: ancestor.title,
-          tags: [...ancestor.tags],
-          metadata: ancestor.getMetadata(),
-          includedInViews: includedInViews(ancestor.views()),
-        })
+        results.push(toResult(ancestor))
       }
       break
     }
     // ... same for all other cases
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-graph.ts` around lines 121 -
235, The switch blocks repeatedly map elements (e.g., items from
element.ancestors(), element.descendants(), element.siblings(),
element.children(), element.incomers(), element.outgoers(), and the parent
variable) into the same result shape pushed into results; extract that mapping
into a small helper (e.g., mapElementToResult or buildQueryResult) that accepts
an Element and returns {id, name, kind, title, tags, metadata, includedInViews},
then replace each push with results.push(mapElementToResult(x)) and call
includedInViews(x.views()) and x.getMetadata() inside the helper so all cases
(including the parent branch) use the same centralized logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/language-server/src/mcp/server/MCPServerFactory.ts`:
- Around line 56-57: The tool descriptions for "query-incomers-graph" and
"query-outgoers-graph" are missing the optional maxNodes parameter; update the
instruction strings in MCPServerFactory (where these tool/help texts are
defined) to include maxNodes? alongside includeIndirect?, maxDepth? and project?
so the input reads e.g. { elementId, includeIndirect?, maxDepth?, maxNodes?,
project? } for both query-incomers-graph and query-outgoers-graph to ensure LLMs
consider node limits when invoking these tools.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.spec.ts`:
- Around line 420-453: The test is passing maxDepth explicitly so it doesn't
exercise the schema default; update the call to the handler returned from
findRelationshipPaths(services.likec4.LanguageServices) to omit the maxDepth
property (call with { sourceId: 'a', targetId: 'e', project: 'default' as
ProjectId } instead of including maxDepth: 3) so the default maxDepth is used,
keep the existing assertions (expect(result.structuredContent) and
expect(paths.length).toBe(0)) to verify the default behavior.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 181-244: The BFS inner loop in find-relationship-paths (inside the
while over queue) can push more than maxPaths because the condition
foundPaths.length < maxPaths is only checked at the while head; modify the inner
for (const rel of currentElement.outgoing()) loop to check foundPaths.length
against maxPaths immediately after adding a path (or before adding) and break
out of the inner loop when maxPaths is reached so no further edges from the same
node can append more paths; update the logic around the foundPaths.push call in
the block that creates PathStep (and similarly before queue.push if you decide
to pre-check) to ensure foundPaths never exceeds maxPaths.

In `@packages/language-server/src/mcp/tools/query-by-metadata.spec.ts`:
- Around line 278-304: The test in query-by-metadata.spec.ts claims to verify
the default matchMode but explicitly passes matchMode: 'exact'; update the test
so it calls the handler returned by
queryByMetadata(services.likec4.LanguageServices) without the matchMode property
in the args object (e.g., pass { key: 'owner', value: 'platform-team', project:
'default' as ProjectId } to the handler) so the codepath uses the default; keep
the same assertions (result.structuredContent and results[0].id === 'frontend')
to validate defaulting to exact match.

In `@packages/language-server/src/mcp/tools/query-graph.spec.ts`:
- Around line 535-582: The test currently queries incomers for element
'frontend' but the model defines no relationships into frontend so the
assertions inside the if (results.length > 0) never run; update the test setup
so incomers returns at least one result by adding a relationship into frontend
(e.g., make backend reference frontend in the model) or change the query to a
relation that will return results (e.g., queryType: 'children' on a parent
element); update the validate() model string accordingly and keep the rest of
the test (handler = queryGraph(...), the call to handler, and the structural
assertions) unchanged.

In `@packages/language-server/src/mcp/tools/query-graph.ts`:
- Around line 113-238: The handler for query-graph (the async (languageServices,
args) => { ... } function) currently pushes all matches for each args.queryType
(ancestors, descendants, siblings, children, parent, incomers, outgoers) without
any cap; add a result limit: define a DEFAULT_LIMIT (e.g. 50) and accept an
optional args.limit, compute const limit = Math.min(args.limit || DEFAULT_LIMIT,
MAX_LIMIT) and enforce it while collecting results by either breaking the loop
when results.length >= limit or using limited iterators and/or slicing the
arrays returned by element.descendants(), element.siblings(),
element.children(), element.incomers(), element.outgoers() and the parent case;
return only up to limit items in the final { results } payload to prevent
unbounded responses.

---

Outside diff comments:
In `@packages/language-server/src/mcp/tools/read-project-summary.ts`:
- Around line 98-105: The example JSON for the "views" array uses "name" but the
actual schema/implementation expect "id"; update the example in
read-project-summary.ts so each view object uses "id" (e.g., "id":
"system-overview") instead of "name" to match the schema and the implementation
(the "views" example block should mirror the schema/implementation's "id"
field).

---

Nitpick comments:
In `@packages/config/src/schema.ts`:
- Around line 82-84: Replace z.any() with z.unknown() in the metadata schema
definition so the line using z.record(z.string(), z.any()).optional().meta(...)
becomes z.record(z.string(), z.unknown()).optional().meta(...); this preserves
runtime validation while changing the inferred TypeScript type to Record<string,
unknown> and improving type-safety for consumers of the metadata field in the
schema.ts metadata definition.

In `@packages/language-server/src/mcp/tools/_common.ts`:
- Around line 49-50: The serializeConfig function currently uses loose any
types; change the parameter type from any to the concrete config interface
exported by `@likec4/config` (e.g., ProjectConfig or the appropriate exported
type) and change the intermediate result variable to
Partial<SerializableProjectConfig> instead of any; update usages inside
serializeConfig (e.g., config.title, config.routes) with narrow type assertions
or optional checks as needed to satisfy the compiler and return a fully
populated SerializableProjectConfig, keeping the function name serializeConfig
and the target SerializableProjectConfig type for locating the change.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 196-236: Extract the duplicated PathStep construction into a small
helper function (e.g., buildPathStep or makePathStep) that takes the
relationship object (rel) and returns the PathStep, then replace both inline
constructions (the ones used when nextId === target.id and when enqueueing for
exploration) with calls to this helper; update usages around foundPaths.push and
the queue push so they use the helper result while preserving source:
rel.source.id, target: rel.target.id and relationship fields (kind, title,
description.text, technology, tags).

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts`:
- Around line 151-195: Extract the duplicated element-processing logic into a
helper (e.g., processElement or processElements) and call it for both
model.elements() and model.deployment.elements(); the helper should accept an
element (or an iterable) plus shared state (results, limit, args, matchMode) and
perform the existing checks and push the same object shape using matches,
getMatchedValue and includedInViews; replace the two for-loops with calls that
iterate the two sources (model.elements() and model.deployment.elements()) and
early-exit when results.length >= limit to preserve current behavior.
- Around line 110-129: The functions matches and getMatchedValue currently
accept mode: string which loses type safety; update both signatures to use the
literal union type for match modes (e.g. 'exists' | 'exact' | 'contains' or the
existing MatchMode type from your schema if available) so the compiler enforces
allowed values, and adjust any call sites if necessary to match the new type;
ensure imports/reference to the schema's MatchMode (or define the explicit
union) is added so both matches and getMatchedValue use the same precise type
instead of string.

In `@packages/language-server/src/mcp/tools/query-by-tags.spec.ts`:
- Around line 351-353: The test currently checks results.length with
toBeGreaterThanOrEqual(2), which is loose; change the assertion to
expect(results).toHaveLength(2) (or expect(results.length).toBe(2)) to enforce
the exact expected count for the array extracted from result.structuredContent
(see the variable results and the earlier
expect(result.structuredContent).toBeDefined() in the test for guidance).

In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Around line 109-110: The function currently collects matches into the results
array and caps at const limit = 50 without telling callers; add a truncated
boolean to the function's output schema and to the returned object (e.g., {
results, truncated }), introduce and initialize let truncated = false, set
truncated = true immediately before breaking out of any loop when results.length
>= limit, and ensure the final return includes this truncated flag alongside
results so callers can detect silent truncation (reference the results and limit
symbols in this file).

In `@packages/language-server/src/mcp/tools/query-graph.ts`:
- Around line 121-235: The switch blocks repeatedly map elements (e.g., items
from element.ancestors(), element.descendants(), element.siblings(),
element.children(), element.incomers(), element.outgoers(), and the parent
variable) into the same result shape pushed into results; extract that mapping
into a small helper (e.g., mapElementToResult or buildQueryResult) that accepts
an Element and returns {id, name, kind, title, tags, metadata, includedInViews},
then replace each push with results.push(mapElementToResult(x)) and call
includedInViews(x.views()) and x.getMetadata() inside the helper so all cases
(including the parent branch) use the same centralized logic.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts`:
- Around line 141-142: The loop currently uses Array.shift() (in the while that
destructures { elementId, depth } from queue.shift()!) which is O(n) per pop;
replace this with an index-pointer pattern: introduce a numeric queueHead = 0,
push items onto the existing queue array as before, and change the loop to run
while (queueHead < queue.length) and read items with queue[queueHead++] instead
of shift(); keep the rest of the logic (enqueuing, depth checks, maxNodes)
unchanged and ensure any length checks use queue.length - queueHead if you need
the number of pending items.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.ts`:
- Around line 141-142: The BFS loop currently uses queue.shift(), which is O(n)
per call; replace the shift-based dequeue with an index-based pointer (e.g.,
maintain a head/index and read queue[head++] while head < queue.length) so
dequeuing becomes O(1) and avoids re-indexing; update the loop in
query-outgoers-graph.ts where queue.shift() is used (the while (queue.length >
0) { const { elementId, depth } = queue.shift()! } block) to use the head
pointer and continue using queue.push(...) for enqueues.

Comment thread packages/language-server/src/mcp/server/MCPServerFactory.ts Outdated
Comment thread packages/language-server/src/mcp/tools/find-relationship-paths.ts
Comment thread packages/language-server/src/mcp/tools/query-by-metadata.spec.ts Outdated
Comment thread packages/language-server/src/mcp/tools/query-graph.spec.ts
Comment thread packages/language-server/src/mcp/tools/query-graph.ts Outdated
@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch 2 times, most recently from 7f0557a to edb1cf6 Compare February 18, 2026 16:02
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch 2 times, most recently from 8d8de96 to c9b7eea Compare February 18, 2026 16:16
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch from c9b7eea to 72a93a6 Compare February 18, 2026 16:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/language-server/src/mcp/tools/read-project-summary.ts (1)

68-106: ⚠️ Potential issue | 🟡 Minor

The example JSON response is missing the new config field.

The config field is documented in the description (lines 28–37), included in outputSchema (line 114), and populated in the implementation (line 161), but the illustrative example object omits it entirely — jumping from "sources" straight to "specification". Consumers reading the example alone will not know config is returned.

Add a config entry to the example, e.g.:

   "sources": [
     "/abs/path/to/workspace/examples/cloud-system/model.c4"
   ],
+  "config": {
+    "name": "cloud-boutique",
+    "title": "Cloud Boutique"
+  },
   "specification": {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/read-project-summary.ts` around lines
68 - 106, The example JSON response in the sample object is missing the
documented and implemented "config" field; update the sample JSON (the example
response literal in read-project-summary.ts) to include a "config" entry
matching the shape described in the docstring/outputSchema and the runtime
population (so consumers see the returned config), e.g. add a top-level "config"
object between "sources" and "specification" that reflects the documented
keys/values used by the implementation.
🧹 Nitpick comments (8)
packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts (4)

91-125: MaxDepth test correctly validates depth-bounded traversal.

Consider also asserting content['truncated'] is false here to confirm that depth-limiting alone doesn't trigger the truncation flag (since per the implementation, truncated should only be set when maxNodes is reached).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 91 - 125, The test for queryOutgoersGraph's maxDepth should also assert
that the traversal wasn't truncated: after calling the handler (the
queryOutgoersGraph(...) returned handler) and extracting content =
result.structuredContent, add an assertion that content['truncated'] is false to
confirm depth limiting alone doesn't set the truncation flag; keep the existing
checks for content['totalNodes'] and content['maxDepth'].

10-247: Missing test coverage for includeIndirect: false.

All six tests pass includeIndirect: true. The queryOutgoersGraph tool accepts an includeIndirect parameter that filters relationships (per the tool summary, it controls whether indirect relationships are included). A test with includeIndirect: false would verify that the filtering logic works correctly and prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 10 - 247, Add a new unit test for queryOutgoersGraph that calls the
returned handler with includeIndirect: false and asserts only direct outgoers
are returned (no indirect nodes), referencing the existing pattern used in tests
that call queryOutgoersGraph(services.likec4.LanguageServices) to obtain the
handler; specifically, build a small model with a chain A -> B -> C, call
handler with elementId 'A' and includeIndirect: false, then assert
structuredContent.nodes contains A and B only, totalNodes equals 2, maxDepth
reflects only direct link, and truncated is false.

240-246: Redundant assertions — intentional but worth noting.

Lines 242–243 assert totalNodes == 3 and Object.keys(nodes).length == 3 independently, making line 246 (totalNodes == Object.keys(nodes).length) trivially true. The invariant assertion on line 246 is the meaningful one; the concrete value checks above it could be considered supplementary. No action needed if the redundancy is intentional for documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 240 - 246, The test contains redundant assertions: it asserts
content['totalNodes'] === 3 and Object.keys(nodes).length === 3, then asserts
content['totalNodes'] === Object.keys(nodes).length; remove or simplify
redundancy by keeping the meaningful invariant check and either drop the
concrete duplicate (remove the expect(content['totalNodes']).toBe(3) or
expect(Object.keys(nodes).length).toBe(3)) or convert one into a comment for
clarity; adjust the assertions around the variables content['truncated'],
content['totalNodes'], and nodes so the test only verifies the invariant
content['totalNodes'] === Object.keys(nodes).length plus the intended concrete
value if you want to assert the exact count.

153-155: Fragile assertion pattern — short-circuit yields a confusing failure.

If content doesn't have a text property, 'text' in content && content.text evaluates to false, and expect(false).toContain(...) produces a misleading error. Split into two assertions for clarity:

Suggested fix
     expect(result.isError).toBe(true)
     const content = result.content![0]!
-    expect('text' in content && content.text).toContain('Element "non-existent" not found')
+    expect(content).toHaveProperty('text')
+    expect((content as { text: string }).text).toContain('Element "non-existent" not found')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 153 - 155, The current assertion uses a short-circuit expression ('text'
in content && content.text) which can evaluate to false and produce a misleading
failure; instead, first assert that content has a text property (e.g.,
expect('text' in content).toBe(true) or expect(content).toHaveProperty('text'))
and then separately assert that content.text contains 'Element "non-existent"
not found' so failures clearly indicate whether the property is missing or the
string check failed; update the assertions around the result and content
variables in query-outgoers-graph.spec.ts accordingly.
packages/language-server/src/mcp/tools/_common.ts (1)

25-25: z.any() weakens the metadata schema.

z.record(z.string(), z.any()) allows arbitrary value types in metadata, which reduces type safety and may let invalid data pass through. Consider z.record(z.string(), z.unknown()) if you truly need open-ended values, or z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])) if the supported types are known.

As per coding guidelines, "Use TypeScript with explicit types in all TypeScript files; avoid using any".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/_common.ts` at line 25, The metadata
field in the Zod schema (the metadata property in the schema defined in
_common.ts) uses z.any() which weakens typing; change it to a safer
alternative—either z.record(z.string(), z.unknown()).optional() to allow
arbitrary but typed-unknown values, or restrict to supported types with
z.record(z.string(), z.union([z.string(), z.number(), z.boolean(),
z.array(z.string())])).optional()—and keep the existing .describe(...) and
.optional() calls intact so the schema remains expressive but type-safe.
packages/language-server/src/mcp/tools/find-relationship-paths.ts (1)

200-214: Duplicated PathStep construction logic.

The same object shape is built in two places (lines 200–210 for found paths, lines 231–241 for queue entries). Consider extracting a helper:

♻️ Suggested refactor
+    const toPathStep = (rel: typeof currentElement extends { outgoing(): Iterable<infer R> } ? R : never): PathStep => ({
+      source: rel.source.id,
+      target: rel.target.id,
+      relationship: {
+        kind: rel.kind,
+        title: rel.title,
+        description: rel.description?.text ?? null,
+        technology: rel.technology,
+        tags: [...rel.tags],
+      },
+    })

Then replace both blocks with const pathStep = toPathStep(rel).

Also applies to: 231-241

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts` around
lines 200 - 214, Duplicate construction of PathStep occurs in the file (used
when pushing into foundPaths and when enqueuing new queue entries); extract a
small pure helper function, e.g. toPathStep(rel: Relationship): PathStep, that
maps rel -> { source: rel.source.id, target: rel.target.id, relationship: {
kind: rel.kind, title: rel.title, description: rel.description.text, technology:
rel.technology, tags: [...rel.tags] } }, then replace both inline constructions
with const pathStep = toPathStep(rel) and reuse that variable for
foundPaths.push(...) and for queue enqueueing to eliminate duplication and keep
semantics identical.
packages/language-server/src/mcp/tools/query-incomers-graph.ts (1)

109-211: Near-identical implementation with query-outgoers-graph.ts.

The handler logic in this file and query-outgoers-graph.ts differ only in the traversal direction (incoming vs outgoing) and field names (incomers vs outgoers). Consider extracting a shared BFS traversal helper parameterized by direction to reduce the ~100 lines of duplication. Not urgent for this PR, but worth tracking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts` around lines
109 - 211, The handler duplicates the BFS traversal in query-incomers-graph.ts
and query-outgoers-graph.ts; extract a shared helper (e.g., bfsTraverseGraph or
traverseGraph) that accepts parameters: model, startElementId, filter, maxDepth,
maxNodes, and traversalDirection ('incoming' | 'outgoing') plus callbacks or
config to map relationship fields (e.g., incomers vs outgoers) and
includedInViews logic; move the queue/visited/maxDepth/truncated logic and node
shape construction into that helper and have the existing handlers call it
(preserving returned shape: target, totalNodes, maxDepth, truncated, nodes)
while using element.incoming(filter) vs element.outgoing(filter) inside the
helper based on traversalDirection. Ensure unique symbols referenced here
(queue, visited, actualMaxDepth, incoming(filter)/outgoing(filter),
incomers/incomerData/outgoers) are replaced by the helper implementation and
update both handlers to delegate to it.
packages/language-server/src/mcp/tools/query-by-tags.ts (1)

99-104: Use Zod schema-level validation instead of invariant for the at-least-one-condition check.

invariant(...) throws an exception that the MCP framework then wraps into an error response — this works but is semantically wrong for input validation (as opposed to model-state assertions like element-not-found). Using a Zod superRefine on the input object keeps validation at the schema boundary and produces structured, consistent error messages.

♻️ Suggested refactor (schema-level validation)

In _common.ts or inline, wrap the three fields in an object schema with a superRefine:

- allOf: z.array(z.string()).optional().describe('...'),
- anyOf: z.array(z.string()).optional().describe('...'),
- noneOf: z.array(z.string()).optional().describe('...'),
- project: projectIdSchema,
// Input object validated at schema level
inputSchema: z.object({
  allOf: z.array(z.string()).optional().describe('Element must have ALL these tags (AND)'),
  anyOf: z.array(z.string()).optional().describe('Element must have ANY of these tags (OR)'),
  noneOf: z.array(z.string()).optional().describe('Element must have NONE of these tags (NOT)'),
  project: projectIdSchema,
}).superRefine((data, ctx) => {
  const hasCondition =
    (data.allOf && data.allOf.length > 0) ||
    (data.anyOf && data.anyOf.length > 0) ||
    (data.noneOf && data.noneOf.length > 0)
  if (!hasCondition) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'At least one condition (allOf, anyOf, or noneOf) must be specified with at least one tag',
    })
  }
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts` around lines 99 -
104, Replace the runtime invariant check in query-by-tags.ts with schema-level
validation using Zod: move the allOf/anyOf/noneOf fields into the input object
schema (e.g., inputSchema) and add a superRefine that computes hasCondition =
(allOf?.length>0) || (anyOf?.length>0) || (noneOf?.length>0) and calls
ctx.addIssue with a custom ZodIssueCode and the same message when false; remove
the existing invariant(...) call and rely on the schema validation to produce
structured errors for the handler that consumes the validated input.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Around line 49-94: Change the loose any types: update the serializeConfig
signature to accept config: Record<string, unknown> and change the accumulator
from const result: any to const result: Partial<SerializableProjectConfig> so
the function documents incremental construction while avoiding any; keep
building properties onto result and return it as SerializableProjectConfig
(cast/validate at return if necessary). Also update the schema usage of z.any()
to z.unknown() to improve type safety (replace z.any() with z.unknown() in the
schema where present).

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 194-248: The code is accessing rel.description.text (invalid) and
storing a MarkdownOrString into PathStep.relationship.description; change both
occurrences where PathStep is constructed (the two pathStep definitions inside
the loop) to call flattenMarkdownOrString(rel.description) so the description
becomes a string | null; also add an import for flattenMarkdownOrString from
`@likec4/core/types` and ensure you replace rel.description.text with
flattenMarkdownOrString(rel.description) in both places (and apply the same fix
in find-relationships.ts:151 as noted).

In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Around line 109-111: The function in
packages/language-server/src/mcp/tools/query-by-tags.ts currently hard-codes
limit = 50 and silently drops results; change the implementation that builds and
returns results (look for the results array and limit constant) to (1) compute
the full match count (totalNodes/totalMatches) before applying the limit or
detect if more items exist beyond the limit, (2) slice the results to the
existing limit as before, and (3) include and return two new fields alongside
results: truncated: boolean (true when more items exist than the limit) and
totalNodes (or totalMatches) containing the full count; also update any code
paths that consume the function result to handle the new truncated/total fields
and adjust tests if present.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.spec.ts`:
- Line 35: Replace all uses of `any` in query-incomers-graph.spec.ts: change the
context casts like `{} as any` to the proper test context type (e.g.,
`RequestHandlerExtra` or `McpToolContext`) by importing that type and using it
where the mock context is passed to the tool; replace `as Record<string, any>`
with the tool output type or `Record<string, unknown>` by deriving the response
type from the tool's output schema/interface; and replace `(n: any)` with the
concrete node type from the node schema (or an inline shape matching the node
interface). Update imports to bring in RequestHandlerExtra/McpToolContext and
the node/output interfaces and adjust the mocked values to satisfy those types.

---

Outside diff comments:
In `@packages/language-server/src/mcp/tools/read-project-summary.ts`:
- Around line 68-106: The example JSON response in the sample object is missing
the documented and implemented "config" field; update the sample JSON (the
example response literal in read-project-summary.ts) to include a "config" entry
matching the shape described in the docstring/outputSchema and the runtime
population (so consumers see the returned config), e.g. add a top-level "config"
object between "sources" and "specification" that reflects the documented
keys/values used by the implementation.

---

Duplicate comments:
In `@packages/language-server/src/mcp/tools/find-relationship-paths.spec.ts`:
- Around line 420-453: Test correctly omits maxDepth and verifies default
behavior in the "should use default maxDepth of 3 when not specified" test that
calls findRelationshipPaths and invokes its handler; no functional change
needed—just keep the existing assertion expect(paths.length).toBe(0) and remove
the stray duplicate review marker/comment text ([duplicate_comment]) from the
test file to avoid noise.

---

Nitpick comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Line 25: The metadata field in the Zod schema (the metadata property in the
schema defined in _common.ts) uses z.any() which weakens typing; change it to a
safer alternative—either z.record(z.string(), z.unknown()).optional() to allow
arbitrary but typed-unknown values, or restrict to supported types with
z.record(z.string(), z.union([z.string(), z.number(), z.boolean(),
z.array(z.string())])).optional()—and keep the existing .describe(...) and
.optional() calls intact so the schema remains expressive but type-safe.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 200-214: Duplicate construction of PathStep occurs in the file
(used when pushing into foundPaths and when enqueuing new queue entries);
extract a small pure helper function, e.g. toPathStep(rel: Relationship):
PathStep, that maps rel -> { source: rel.source.id, target: rel.target.id,
relationship: { kind: rel.kind, title: rel.title, description:
rel.description.text, technology: rel.technology, tags: [...rel.tags] } }, then
replace both inline constructions with const pathStep = toPathStep(rel) and
reuse that variable for foundPaths.push(...) and for queue enqueueing to
eliminate duplication and keep semantics identical.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Around line 99-104: Replace the runtime invariant check in query-by-tags.ts
with schema-level validation using Zod: move the allOf/anyOf/noneOf fields into
the input object schema (e.g., inputSchema) and add a superRefine that computes
hasCondition = (allOf?.length>0) || (anyOf?.length>0) || (noneOf?.length>0) and
calls ctx.addIssue with a custom ZodIssueCode and the same message when false;
remove the existing invariant(...) call and rely on the schema validation to
produce structured errors for the handler that consumes the validated input.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts`:
- Around line 109-211: The handler duplicates the BFS traversal in
query-incomers-graph.ts and query-outgoers-graph.ts; extract a shared helper
(e.g., bfsTraverseGraph or traverseGraph) that accepts parameters: model,
startElementId, filter, maxDepth, maxNodes, and traversalDirection ('incoming' |
'outgoing') plus callbacks or config to map relationship fields (e.g., incomers
vs outgoers) and includedInViews logic; move the
queue/visited/maxDepth/truncated logic and node shape construction into that
helper and have the existing handlers call it (preserving returned shape:
target, totalNodes, maxDepth, truncated, nodes) while using
element.incoming(filter) vs element.outgoing(filter) inside the helper based on
traversalDirection. Ensure unique symbols referenced here (queue, visited,
actualMaxDepth, incoming(filter)/outgoing(filter),
incomers/incomerData/outgoers) are replaced by the helper implementation and
update both handlers to delegate to it.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts`:
- Around line 91-125: The test for queryOutgoersGraph's maxDepth should also
assert that the traversal wasn't truncated: after calling the handler (the
queryOutgoersGraph(...) returned handler) and extracting content =
result.structuredContent, add an assertion that content['truncated'] is false to
confirm depth limiting alone doesn't set the truncation flag; keep the existing
checks for content['totalNodes'] and content['maxDepth'].
- Around line 10-247: Add a new unit test for queryOutgoersGraph that calls the
returned handler with includeIndirect: false and asserts only direct outgoers
are returned (no indirect nodes), referencing the existing pattern used in tests
that call queryOutgoersGraph(services.likec4.LanguageServices) to obtain the
handler; specifically, build a small model with a chain A -> B -> C, call
handler with elementId 'A' and includeIndirect: false, then assert
structuredContent.nodes contains A and B only, totalNodes equals 2, maxDepth
reflects only direct link, and truncated is false.
- Around line 240-246: The test contains redundant assertions: it asserts
content['totalNodes'] === 3 and Object.keys(nodes).length === 3, then asserts
content['totalNodes'] === Object.keys(nodes).length; remove or simplify
redundancy by keeping the meaningful invariant check and either drop the
concrete duplicate (remove the expect(content['totalNodes']).toBe(3) or
expect(Object.keys(nodes).length).toBe(3)) or convert one into a comment for
clarity; adjust the assertions around the variables content['truncated'],
content['totalNodes'], and nodes so the test only verifies the invariant
content['totalNodes'] === Object.keys(nodes).length plus the intended concrete
value if you want to assert the exact count.
- Around line 153-155: The current assertion uses a short-circuit expression
('text' in content && content.text) which can evaluate to false and produce a
misleading failure; instead, first assert that content has a text property
(e.g., expect('text' in content).toBe(true) or
expect(content).toHaveProperty('text')) and then separately assert that
content.text contains 'Element "non-existent" not found' so failures clearly
indicate whether the property is missing or the string check failed; update the
assertions around the result and content variables in
query-outgoers-graph.spec.ts accordingly.

Comment thread packages/language-server/src/mcp/tools/_common.ts Outdated
Comment thread packages/language-server/src/mcp/tools/find-relationship-paths.ts Outdated
Comment thread packages/language-server/src/mcp/tools/query-by-tags.ts Outdated
Comment thread packages/language-server/src/mcp/tools/query-incomers-graph.spec.ts
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/language-server/src/mcp/tools/read-project-summary.ts (1)

67-105: ⚠️ Potential issue | 🟡 Minor

Example response in the description is missing the new config field.

Lines 28–37 document the config field in the response, but the JSON example (lines 67–105) omits it entirely, creating an inconsistency in the tool's self-description.

📝 Example fix — add `config` block to the JSON example
   "sources": [
     "/abs/path/to/workspace/examples/cloud-system/model.c4"
   ],
+  "config": {
+    "name": "cloud-system",
+    "title": "Cloud Boutique"
+  },
   "specification": {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/read-project-summary.ts` around lines
67 - 105, The JSON example under "Example response" is missing the documented
config field; update that example to include a top-level "config" object
consistent with the documented shape (e.g., keys and types described alongside
"specification"), placing it alongside "title", "folder", "sources", and
"specification" so the example matches the docs and tools referencing
read-project-summary.ts's example response.
🧹 Nitpick comments (8)
packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts (3)

242-246: Line 246 is a redundant assertion.

Lines 242 (totalNodes === 3) and 243 (nodes.length === 3) already establish equality, making line 246 (totalNodes === nodes.length) a tautology. Remove one or keep only the relational assertion for clarity.

♻️ Proposed simplification
-   expect(content['totalNodes']).toBe(3)
-   expect(Object.keys(nodes).length).toBe(3)
-
-   // totalNodes must match actual node count even when truncated
-   expect(content['totalNodes']).toBe(Object.keys(nodes).length)
+   // totalNodes must match actual node count even when truncated
+   expect(Object.keys(nodes).length).toBe(3)
+   expect(content['totalNodes']).toBe(Object.keys(nodes).length)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 242 - 246, Remove the redundant tautological assertion: in the test that
uses content['totalNodes'] and nodes, delete the final
expect(content['totalNodes']).toBe(Object.keys(nodes).length) (or alternatively
remove the earlier numeric checks and keep only the relational check) so the
test no longer asserts the same equality twice; locate where
content['totalNodes'] and nodes are asserted (the expect calls around totalNodes
and Object.keys(nodes).length) and keep either the concrete numeric assertions
or the relational assertion, but not both.

40-40: Replace Record<string, any> with a typed assertion per coding guidelines.

content['nodes'] as Record<string, any> appears in three test cases. If the structuredContent type (or the node shape) is exported from query-outgoers-graph.ts, prefer that. Otherwise, at minimum, extract a local interface for the node shape used within each test so the property accesses are type-checked. As per coding guidelines, avoid using any.

Also applies to: 193-193, 238-238

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` at line
40, Replace the unsafe any assertion for content['nodes'] in
query-outgoers-graph.spec.ts by using a concrete typed shape: either import the
exported structuredContent or node type from query-outgoers-graph.ts (e.g.,
StructuredContent or OutgoerNode) and assert with that type, or declare a local
interface (e.g., TestNode or LocalOutgoerNode) that lists the properties the
tests access and use that instead of Record<string, any> for the nodes variable;
update all three occurrences (the lines referencing content['nodes']) so
property accesses are type-checked and no `any` is used.

46-58: consumer1 node goes unverified in the complete-outgoers-graph test.

source has two direct outgoers (consumer1 and consumer2), but only consumer2 and consumer3 have their depth and outgoers asserted. Adding a check for consumer1 (expected: depth === 1, outgoers.length === 0) would make the test exhaustive.

✅ Proposed addition
    expect(nodes['consumer3'].outgoers).toHaveLength(0)
+
+   expect(nodes['consumer1']).toBeDefined()
+   expect(nodes['consumer1'].depth).toBe(1)
+   expect(nodes['consumer1'].outgoers).toHaveLength(0)
  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 46 - 58, Add assertions to verify the missing consumer1 node in the
complete-outgoers-graph test: after the existing checks for nodes['consumer2']
and nodes['consumer3'], assert that nodes['consumer1'] is defined, that
nodes['consumer1'].depth === 1, and that nodes['consumer1'].outgoers has length
0 (and optionally that nodes['consumer1'].outgoers items have elementId if you
want symmetry with other checks); this ensures nodes['consumer1'] is validated
alongside nodes['source'], nodes['consumer2'], and nodes['consumer3'] in
query-outgoers-graph.spec.ts.
packages/language-server/src/mcp/tools/query-by-metadata.ts (2)

110-129: Use the union type from the schema instead of bare string for mode parameter.

Both matches and getMatchedValue accept mode: string, losing the exhaustiveness guarantees of the matchModeSchema enum. Use z.infer<typeof matchModeSchema> (or the literal union 'exact' | 'contains' | 'exists') for the parameter type so the default branch becomes provably unreachable.

♻️ Suggested fix
-  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: string): boolean => {
+  type MatchMode = z.infer<typeof matchModeSchema>
+  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: MatchMode): boolean => {

Apply the same change to getMatchedValue on line 132.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts` around lines 110
- 129, The functions matches and getMatchedValue declare mode as plain string
losing exhaustiveness; change the parameter type to the union from your schema
(use z.infer<typeof matchModeSchema> or the literal union 'exact' | 'contains' |
'exists') for both functions, update any imports/types accordingly, and then
remove or mark the switch default as unreachable (or add an assertNever branch)
so the TypeScript compiler can verify exhaustiveness.

152-195: Element and deployment-node search loops are nearly identical — consider extracting a shared helper.

Lines 152-171 and 174-195 contain the same search-match-push logic, differing only in the iterable source (model.elements() vs model.deployment.elements()). Extracting a helper would reduce duplication and ensure future changes apply uniformly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts` around lines 152
- 195, Extract the duplicated search/match/push logic into a helper (e.g.,
processElements or searchMetadataInIterable) that accepts an iterable of
elements and closes over args, matchMode, results and limit; move the body that
reads element.getMetadata(), checks args.key in metadata, uses matches(),
getMatchedValue(), includedInViews(), and pushes the result object into this
helper, then call it twice with model.elements() and model.deployment.elements()
(preserving the early limit checks inside the helper) so both loops share the
same implementation and avoid duplication.
packages/language-server/src/mcp/tools/find-relationship-paths.ts (1)

138-145: Ancestor/descendant rejection may be overly restrictive.

The invariant on lines 142-145 rejects queries where source is an ancestor/descendant of target. While this avoids nonsensical results for hierarchical containment, it also prevents discovering paths between nested elements that have explicit relationships through intermediate elements outside the hierarchy. Consider whether this should be a soft filter (return empty paths) rather than a hard error, or document the rationale more clearly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts` around
lines 138 - 145, The current hard invariant using source.id !== target.id and
the ancestor check via source.isAncestorOf(target) / target.isAncestorOf(source)
should be relaxed: replace the second invariant that throws on parent-child
relationships with a soft guard in the findRelationshipPaths flow that returns
an empty array (or a documented Result type) instead of throwing, or at minimum
log and return no paths; keep the identity invariant (source.id !== target.id)
but change the ancestor/descendant check (the call sites of source.isAncestorOf
and target.isAncestorOf and the invariant call) to a conditional early return of
an empty result so callers can handle “no paths” cases rather than crashing.
packages/language-server/src/mcp/tools/query-incomers-graph.ts (1)

197-200: Same element can be enqueued multiple times before it is visited.

When multiple already-processed nodes share an unvisited incomer, that incomer is pushed to the queue once per referencing node (since !visited.has(...) is true for all of them at enqueue time). This is safe — the visited.has check at dequeue time discards the duplicates — but it causes wasted queue entries in large, fan-in-heavy graphs.

♻️ Proposed fix — track queued elements separately
+  const queued = new Set<string>([args.elementId])
   const queue: Array<{ elementId: string; depth: number }> = [{ elementId: args.elementId, depth: 0 }]
   ...
   for (const incomerData of incomersData) {
-    if (!visited.has(incomerData.elementId)) {
+    if (!visited.has(incomerData.elementId) && !queued.has(incomerData.elementId)) {
+      queued.add(incomerData.elementId)
       queue.push({ elementId: incomerData.elementId, depth: depth + 1 })
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts` around lines
197 - 200, The loop that pushes incomers into queue (inside the for..of over
incomersData) can enqueue the same element multiple times because only visited
is checked; introduce a separate Set (e.g., queued) and, when you push an
elementId into queue (the same queue variable used later), first check
queued.has(elementId) and add it to queued immediately upon enqueueing; keep the
existing visited check at dequeue time unchanged. Update the code paths that
push into queue (the loop referencing incomersData and any other enqueue sites)
to use queued to avoid duplicate queue entries while preserving the current
dequeue-time visited semantics.
packages/language-server/src/mcp/tools/find-relationship-paths.spec.ts (1)

10-572: Consider adding a test for the 100-path cap (maxPaths).

The find-relationship-paths.ts implementation hard-caps results at 100 paths. The suite has no test to verify this ceiling — a dense graph with >100 source→target paths should produce exactly 100 results and not overflow.

Would you like me to draft a test case that constructs a graph with more than 100 distinct paths and asserts the result is capped at 100?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.spec.ts`
around lines 10 - 572, Add a new test that verifies the 100-path cap by
constructing a dense graph producing more than 100 distinct source→target paths,
invoking the handler returned by
findRelationshipPaths(services.likec4.LanguageServices) and asserting the
returned structuredContent.paths array has length exactly 100; specifically,
build a graph (e.g., layered nodes with many parallel edges) where the number of
unique paths from a chosen sourceId to targetId exceeds 100, call handler with
maxDepth large enough and check result.structuredContent!['paths'].length ===
100 and optionally that no duplicate path objects are returned to confirm the
cap in find-relationship-paths.ts is enforced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Around line 155-170: The loop over model.deployment.elements() can yield
DeployedInstance items which have kind?: never, so before reading
deploymentElement.kind (and other DeploymentNode-only fields) filter using the
existing type guard isDeploymentNode(): only when
isDeploymentNode(deploymentElement) is true push the result; otherwise skip
DeployedInstance items. Update the for loop that currently calls matchesTags()
and includedInViews() to first check isDeploymentNode(deploymentElement) and
then use deploymentElement.kind, name, title, getMetadata(), and views() safely
when building the results array.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts`:
- Around line 183-201: When storing a node's incomers in nodes[elementId],
filter incomersData so it only includes entries whose target nodes actually
exist in nodes (i.e., replace storing the full incomersData with
incomersData.filter(d => nodes[d.elementId]) or similar) to avoid dangling
references when BFS truncates due to visited.size >= maxNodes; keep the
queue/visited logic (queue.push and visited checks) unchanged so traversal still
attempts to discover more nodes, but only persist incomers that can be resolved
from nodes.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts`:
- Around line 1-3: The file contains a third-party copyright header string
"Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." (and
a note that it was generated with Claude Code) that may be incorrect for this
upstream repo; update the PR by removing or replacing that header with the
repository's standard OSS header (or a neutral contributor header), record the
file provenance in the commit message/PR description (mention "generated with
Claude Code" and the contributor), and add a maintainer/legal review request so
the project can confirm the IP attribution and contributor license status before
merging.

---

Outside diff comments:
In `@packages/language-server/src/mcp/tools/read-project-summary.ts`:
- Around line 67-105: The JSON example under "Example response" is missing the
documented config field; update that example to include a top-level "config"
object consistent with the documented shape (e.g., keys and types described
alongside "specification"), placing it alongside "title", "folder", "sources",
and "specification" so the example matches the docs and tools referencing
read-project-summary.ts's example response.

---

Duplicate comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Around line 49-94: serializeConfig currently uses unsafe any types for the
input and intermediate result; update the signature to accept a properly typed
config (replace config: any with the appropriate input type or unknown and
narrow it) and change const result: any to SerializableProjectConfig (or build a
typed partial and cast at the end), ensuring fields like include, manualLayouts,
and styles are validated/typed before property access; additionally replace any
remaining z.any() usages elsewhere in this module with z.unknown() per the prior
review. Ensure all property reads (e.g., config.include.paths,
config.manualLayouts.outDir, config.styles.theme) are guarded or type-narrowed
so TypeScript is satisfied and no implicit any remains.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Line 206: The code is accessing a non-existent property rel.description.text;
update both occurrences (the one at description: rel.description.text and the
other on line 237) to safely read the MarkdownOrString union (which has txt/md)
and handle null/undefined — e.g., use rel.description?.txt ??
rel.description?.md ?? '' or a small helper (e.g., normalizeMarkdownOrString) to
extract a string; ensure you adjust any type annotations or null checks in
find-relationship-paths.ts so rel.description access is safe and won’t throw at
runtime.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Around line 109-173: The results list is being cut off at a hard limit of 50
with no indication to callers; update the search logic in query-by-tags.ts (the
matchesTags helper and the two loops over model.elements() and
model.deployment.elements()) to compute the full count of matching items while
still only pushing up to limit into results, then return an object like {
results, truncated: boolean, totalMatches: number } where truncated is true when
totalMatches > limit; ensure you continue scanning both element sources to
compute totalMatches (but stop adding to results after reaching limit) and
update any callers/types expecting the old return.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.spec.ts`:
- Line 35: Replace the unsafe "any" casts in the test file by defining minimal
test-specific types/interfaces and using those instead of "{} as any" and "as
Record<string, any>" and avoid "(n: any)"; specifically create a TestContext (or
reuse the real Context/State type) and use Partial<TestContext> or
jest.Mocked<TestContext> for the "{} as any" occurrences, replace "as
Record<string, any>" with Record<string, unknown> or a concrete map type
matching what the tests exercise, and change the "(n: any)" parameter to the
real Node/Vertex type used by query-incomers-graph (or a TestNode interface) so
you get proper typing; update imports/types at the top of the spec and adjust
assertions/mocks to satisfy the narrower types.

---

Nitpick comments:
In `@packages/language-server/src/mcp/tools/find-relationship-paths.spec.ts`:
- Around line 10-572: Add a new test that verifies the 100-path cap by
constructing a dense graph producing more than 100 distinct source→target paths,
invoking the handler returned by
findRelationshipPaths(services.likec4.LanguageServices) and asserting the
returned structuredContent.paths array has length exactly 100; specifically,
build a graph (e.g., layered nodes with many parallel edges) where the number of
unique paths from a chosen sourceId to targetId exceeds 100, call handler with
maxDepth large enough and check result.structuredContent!['paths'].length ===
100 and optionally that no duplicate path objects are returned to confirm the
cap in find-relationship-paths.ts is enforced.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 138-145: The current hard invariant using source.id !== target.id
and the ancestor check via source.isAncestorOf(target) /
target.isAncestorOf(source) should be relaxed: replace the second invariant that
throws on parent-child relationships with a soft guard in the
findRelationshipPaths flow that returns an empty array (or a documented Result
type) instead of throwing, or at minimum log and return no paths; keep the
identity invariant (source.id !== target.id) but change the ancestor/descendant
check (the call sites of source.isAncestorOf and target.isAncestorOf and the
invariant call) to a conditional early return of an empty result so callers can
handle “no paths” cases rather than crashing.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts`:
- Around line 110-129: The functions matches and getMatchedValue declare mode as
plain string losing exhaustiveness; change the parameter type to the union from
your schema (use z.infer<typeof matchModeSchema> or the literal union 'exact' |
'contains' | 'exists') for both functions, update any imports/types accordingly,
and then remove or mark the switch default as unreachable (or add an assertNever
branch) so the TypeScript compiler can verify exhaustiveness.
- Around line 152-195: Extract the duplicated search/match/push logic into a
helper (e.g., processElements or searchMetadataInIterable) that accepts an
iterable of elements and closes over args, matchMode, results and limit; move
the body that reads element.getMetadata(), checks args.key in metadata, uses
matches(), getMatchedValue(), includedInViews(), and pushes the result object
into this helper, then call it twice with model.elements() and
model.deployment.elements() (preserving the early limit checks inside the
helper) so both loops share the same implementation and avoid duplication.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts`:
- Around line 197-200: The loop that pushes incomers into queue (inside the
for..of over incomersData) can enqueue the same element multiple times because
only visited is checked; introduce a separate Set (e.g., queued) and, when you
push an elementId into queue (the same queue variable used later), first check
queued.has(elementId) and add it to queued immediately upon enqueueing; keep the
existing visited check at dequeue time unchanged. Update the code paths that
push into queue (the loop referencing incomersData and any other enqueue sites)
to use queued to avoid duplicate queue entries while preserving the current
dequeue-time visited semantics.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts`:
- Around line 242-246: Remove the redundant tautological assertion: in the test
that uses content['totalNodes'] and nodes, delete the final
expect(content['totalNodes']).toBe(Object.keys(nodes).length) (or alternatively
remove the earlier numeric checks and keep only the relational check) so the
test no longer asserts the same equality twice; locate where
content['totalNodes'] and nodes are asserted (the expect calls around totalNodes
and Object.keys(nodes).length) and keep either the concrete numeric assertions
or the relational assertion, but not both.
- Line 40: Replace the unsafe any assertion for content['nodes'] in
query-outgoers-graph.spec.ts by using a concrete typed shape: either import the
exported structuredContent or node type from query-outgoers-graph.ts (e.g.,
StructuredContent or OutgoerNode) and assert with that type, or declare a local
interface (e.g., TestNode or LocalOutgoerNode) that lists the properties the
tests access and use that instead of Record<string, any> for the nodes variable;
update all three occurrences (the lines referencing content['nodes']) so
property accesses are type-checked and no `any` is used.
- Around line 46-58: Add assertions to verify the missing consumer1 node in the
complete-outgoers-graph test: after the existing checks for nodes['consumer2']
and nodes['consumer3'], assert that nodes['consumer1'] is defined, that
nodes['consumer1'].depth === 1, and that nodes['consumer1'].outgoers has length
0 (and optionally that nodes['consumer1'].outgoers items have elementId if you
want symmetry with other checks); this ensures nodes['consumer1'] is validated
alongside nodes['source'], nodes['consumer2'], and nodes['consumer3'] in
query-outgoers-graph.spec.ts.

Comment thread packages/language-server/src/mcp/tools/query-by-tags.ts
Comment thread packages/language-server/src/mcp/tools/query-incomers-graph.ts Outdated
Comment thread packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
packages/language-server/src/mcp/tools/query-graph.ts (2)

126-126: Add an explicit type annotation for results.

const results = [] infers as any[]. Adding a type prevents accidentally pushing malformed objects and aligns with the "avoid using any" guideline.

♻️ Suggested type
-  const results = []
+  const results: Array<{
+    id: string
+    name: string
+    kind: string
+    title: string
+    tags: string[]
+    metadata: Record<string, string | string[]>
+    includedInViews: z.infer<typeof includedInViewsSchema>
+  }> = []

Alternatively, extract a named type/interface for reuse across sibling tools.

As per coding guidelines, "avoid using any" in TypeScript files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-graph.ts` at line 126, The local
variable results is declared as const results = [] which types as any[]; change
it to an explicitly typed array (e.g., results: YourResultType[] or
Array<YourResultType>) to avoid any and ensure only properly shaped objects are
pushed; if no suitable type exists, define a named interface/type (e.g.,
QueryGraphResult) and use that for results so other functions in query-graph.ts
and sibling tools can reuse it.

130-267: Consider extracting the repeated result-building logic into a helper.

The same object construction ({ id, name, kind, title, tags, metadata, includedInViews }) is duplicated across all 7 switch branches. A small helper would reduce the duplication from ~70 lines to ~7 lines of mapping logic and make it easier to add new fields in the future.

♻️ Example helper
const toResult = (el: ElementModel) => ({
  id: el.id,
  name: el.name,
  kind: el.kind,
  title: el.title,
  tags: [...el.tags],
  metadata: el.getMetadata(),
  includedInViews: includedInViews(el.views()),
})

Then each branch becomes:

case 'ancestors': {
  for (const ancestor of element.ancestors()) {
    if (results.length >= MAX_RESULTS) { truncated = true; break }
    results.push(toResult(ancestor))
  }
  break
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-graph.ts` around lines 130 -
267, The switch branches duplicate the same result object construction for
elements (id, name, kind, title, tags, metadata, includedInViews); create a
small helper (e.g., toResult(el: ElementModel)) that returns that object and
replace the repeated object literal in each branch (ancestors, descendants,
siblings, children, parent, incomers, outgoers) with
results.push(toResult(...)); ensure the helper calls includedInViews(el.views())
and spreads el.tags and use it for the parent branch as well to remove the
duplication.
packages/language-server/src/mcp/tools/query-by-tags.ts (1)

109-109: Add an explicit type annotation for results.

Same as in query-graph.tsconst results = [] infers as any[].

As per coding guidelines, "avoid using any" in TypeScript files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts` at line 109, The
local variable "results" is currently inferred as any[]; update its declaration
to include an explicit type annotation matching the result type used in
query-graph.ts (e.g., the same DocumentSearchResult[] or SearchResult[] type
used there) so it no longer becomes any. Locate the "results" const in
query-by-tags.ts (in the queryByTags function) and change "const results = []"
to "const results: <SameResultTypeAsInQueryGraph> = []" (copy the exact type
identifier from query-graph.ts) and ensure any imports for that type are added.
packages/config/src/schema.ts (1)

82-84: Consider z.unknown() instead of z.any() for safer type inference.

z.any() causes the inferred TypeScript type to be Record<string, any>, which disables type checking on consumers. z.unknown() accepts the same runtime values but infers Record<string, unknown>, preserving type safety downstream.

♻️ Suggested change
-  metadata: z.record(z.string(), z.any())
+  metadata: z.record(z.string(), z.unknown())

Note: the same z.any() pattern is used in packages/language-server/src/mcp/tools/_common.ts line 24 for projectConfigSchema.metadata — consider aligning both if you adopt this change.

As per coding guidelines, "avoid using any" applies to TypeScript files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/schema.ts` around lines 82 - 84, The schema uses
z.record(z.string(), z.any()) for the metadata field which infers Record<string,
any>; change this to z.record(z.string(), z.unknown()) so the inferred type
becomes Record<string, unknown> and preserves type safety; update the
occurrences where projectConfigSchema.metadata (and any other places using
z.any() for metadata, e.g., the symbol projectConfigSchema in _common.ts) to use
z.unknown() as well to keep schemas aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Around line 54-62: The current truthy checks on config.title and
config.contactPerson in _common.ts will drop intentionally-set empty strings;
change the guards to check for undefined (e.g., config.title !== undefined and
config.contactPerson !== undefined) so explicit empty-string values are
preserved when assigning to result.title and result.contactPerson; do the same
for config.metadata if you need to distinguish empty values from unset, using
strict undefined checks rather than truthy checks.

In `@packages/language-server/src/mcp/tools/query-graph.ts`:
- Line 6: The local variable const results = [] is missing an explicit type and
should be annotated to avoid an implicit any; update the declaration of results
(and/or introduce a named type at the top of the file) to the provided shape—an
array of objects with id: string, name: string, kind: string, title: string,
tags: string[], metadata: Record<string, string | string[]>, and
includedInViews: ReturnType<typeof includedInViews>—or create a named
interface/type (e.g., QueryNode or ResultItem) and use Array<ResultItem> for
results to improve readability and satisfy the lint rule.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.ts`:
- Around line 141-210: When traversal was truncated (truncated = true) some
nodes' outgoers (outgoersData -> nodes[...].outgoers) can reference elementId
values that were never visited/added to the nodes map; after the BFS loop in
query-outgoers-graph.ts, add a post-processing pass that iterates over nodes and
for each node.filters its outgoers array to only keep entries whose elementId
exists in the nodes map (or remove/mark them), ensuring callers doing
nodes[outgoer.elementId] no longer get undefined; reference the
variables/functions queue, visited, nodes, outgoersData, truncated and perform
the cleanup only when truncated is true.

---

Duplicate comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Around line 49-94: serializeConfig currently uses loose any types (config:
any, result: any) and relies on z.any() elsewhere; change the signature to
accept a well-defined input type (e.g., ProjectConfig or a Zod-inferred type
from your schema) and type the return as SerializableProjectConfig so TypeScript
enforces fields; replace z.any() usages in the related schema with concrete Zod
types (string(), object(), array(), boolean(), etc.) or refine unions where
appropriate; update serializeConfig to access optional properties in a type-safe
way (using Optional chaining and explicit defaults) and construct result as a
SerializableProjectConfig without using any so the compiler validates all
transformed fields (refer to serializeConfig and SerializableProjectConfig in
this file).

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 200-210: The PathStep construction is currently reading
rel.description.text which is invalid for MarkdownOrString|null|undefined;
import flattenMarkdownOrString from `@likec4/core/types` and replace direct .text
access with flattenMarkdownOrString(rel.description) when building the PathStep
objects (both occurrences where PathStep is constructed, e.g., inside the block
creating pathStep and the second PathStep construction), ensuring you pass the
result into the relationship.description field and handle the possibly undefined
return as the existing code expects.

In `@packages/language-server/src/mcp/tools/query-by-metadata.spec.ts`:
- Around line 294-304: The test passes matchMode: undefined which still sets the
property on the args object; update the call to the handler returned by
queryByMetadata(services.likec4.LanguageServices) to omit the matchMode property
entirely (i.e., pass { key: 'owner', value: 'platform-team', project: 'default'
as ProjectId } instead of including matchMode), so the schema's default branch
is exercised without an explicit undefined property.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Around line 155-170: The loop is currently pushing deployment elements that
may be DeployedInstance (which has kind?: never) into results, violating the
output schema expecting kind: string; add a type guard to skip non-deployment
nodes—use the existing isDeploymentNode(deploymentElement) (or equivalent)
before constructing the result object in the for loop so only nodes with a
defined kind are pushed; keep the same fields (id, name, kind, title, tags,
metadata, includedInViews) and ensure includedInViews(deploymentElement.views())
and matchesTags(tags) logic remain unchanged.
- Around line 109-111: The function currently builds results and enforces a hard
limit (const limit = 50) but returns only results, silently truncating; update
the code paths that use results (including the block around const results/limit
and the other occurrence near line 173) to compute a truncated boolean (e.g.,
truncated = totalMatches > limit), return the sliced results array
(results.slice(0, limit)) and include truncated in the response object (matching
query-graph.ts’s { results, truncated } shape) so callers can distinguish
“exactly 50” from “50 of N.”

In `@packages/language-server/src/mcp/tools/query-graph.spec.ts`:
- Around line 535-584: The test 'should include metadata and tags in results'
still uses a guarded if (results.length > 0) which allows a silent regression;
make the assertions unconditional by asserting results.length is > 0 and then
checking results[0].id, results[0].tags is an Array and results[0].metadata is
an Object; update the assertions in this test (the handler returned by
queryGraph(services.likec4.LanguageServices) and the
result.structuredContent!['results']) to remove the conditional and assert
presence and types directly.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts`:
- Around line 183-210: When BFS stops early due to maxNodes and truncated is
true, some nodes' incomers arrays may reference elementIds that were never added
to nodes; fix this by (1) only pushing incomers onto queue when not truncated
and when the incomer elementId hasn't been visited (refer to queue, visited,
incomersData, maxNodes, truncated), and (2) after the search finishes (before
returning nodes), iterate the stored nodes (nodes[elementId]) and filter each
node.incomers to keep only entries whose elementId exists in nodes (or visited)
so there are no dangling references.

---

Nitpick comments:
In `@packages/config/src/schema.ts`:
- Around line 82-84: The schema uses z.record(z.string(), z.any()) for the
metadata field which infers Record<string, any>; change this to
z.record(z.string(), z.unknown()) so the inferred type becomes Record<string,
unknown> and preserves type safety; update the occurrences where
projectConfigSchema.metadata (and any other places using z.any() for metadata,
e.g., the symbol projectConfigSchema in _common.ts) to use z.unknown() as well
to keep schemas aligned.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Line 109: The local variable "results" is currently inferred as any[]; update
its declaration to include an explicit type annotation matching the result type
used in query-graph.ts (e.g., the same DocumentSearchResult[] or SearchResult[]
type used there) so it no longer becomes any. Locate the "results" const in
query-by-tags.ts (in the queryByTags function) and change "const results = []"
to "const results: <SameResultTypeAsInQueryGraph> = []" (copy the exact type
identifier from query-graph.ts) and ensure any imports for that type are added.

In `@packages/language-server/src/mcp/tools/query-graph.ts`:
- Line 126: The local variable results is declared as const results = [] which
types as any[]; change it to an explicitly typed array (e.g., results:
YourResultType[] or Array<YourResultType>) to avoid any and ensure only properly
shaped objects are pushed; if no suitable type exists, define a named
interface/type (e.g., QueryGraphResult) and use that for results so other
functions in query-graph.ts and sibling tools can reuse it.
- Around line 130-267: The switch branches duplicate the same result object
construction for elements (id, name, kind, title, tags, metadata,
includedInViews); create a small helper (e.g., toResult(el: ElementModel)) that
returns that object and replace the repeated object literal in each branch
(ancestors, descendants, siblings, children, parent, incomers, outgoers) with
results.push(toResult(...)); ensure the helper calls includedInViews(el.views())
and spreads el.tags and use it for the parent branch as well to remove the
duplication.

Comment thread packages/language-server/src/mcp/tools/_common.ts Outdated
Comment thread packages/language-server/src/mcp/tools/query-graph.ts
Comment thread packages/language-server/src/mcp/tools/query-outgoers-graph.ts Outdated
@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch from 72a93a6 to 2f660bf Compare February 18, 2026 17:13
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch from 2f660bf to 8e70810 Compare February 18, 2026 17:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
apps/docs/src/content/docs/tooling/mcp.mdx (1)

158-173: Consider unifying description style across all table rows.

The descriptions alternate between verb-led imperatives/present-tense ("List all…", "Search elements…", "Opens the…") and bare noun phrases ("Full element details…", "Recursive BFS graph…", "Tag filtering with…"). Picking one style throughout makes the table easier to scan.

✏️ Example: normalising to verb-led descriptions
-| `read-project-summary` | Project specification, configuration, all elements, deployment nodes and views |
+| `read-project-summary` | Returns project specification, configuration, all elements, deployment nodes and views |
-| `read-element` | Full element details including relationships, views, deployments, metadata |
+| `read-element` | Returns full element details including relationships, views, deployments, metadata |
-| `read-deployment` | Details of a deployment node or deployed instance |
+| `read-deployment` | Returns details of a deployment node or deployed instance |
-| `read-view` | Full view details (nodes/edges) and source location |
+| `read-view` | Returns full view details (nodes/edges) and source location |
-| `find-relationships` | Direct and indirect relationships between two elements |
+| `find-relationships` | Finds direct and indirect relationships between two elements |
-| `query-incomers-graph` | Recursive BFS graph of all upstream dependencies/producers |
+| `query-incomers-graph` | Returns a recursive BFS graph of all upstream dependencies/producers |
-| `query-outgoers-graph` | Recursive BFS graph of all downstream consumers/dependents |
+| `query-outgoers-graph` | Returns a recursive BFS graph of all downstream consumers/dependents |
-| `query-by-tags` | Tag filtering with boolean logic (allOf, anyOf, noneOf) |
+| `query-by-tags` | Filters elements by tags with boolean logic (allOf, anyOf, noneOf) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/docs/src/content/docs/tooling/mcp.mdx` around lines 158 - 173, The table
of MCP commands mixes verb-led imperative descriptions and bare noun phrases;
normalize all descriptions to a consistent verb-led present-tense style (e.g.,
"List all…", "Read full element details…", "Search elements…", "Open the view…")
by editing each row for commands like `list-projects`, `read-project-summary`,
`search-element`, `read-element`, `read-deployment`, `read-view`,
`find-relationships`, `query-graph`, `query-incomers-graph`,
`query-outgoers-graph`, `query-by-metadata`, `query-by-tags`,
`find-relationship-paths`, and `open-view` so every Description cell starts with
a verb and follows the same tense/voice for consistency and scanability.
packages/config/src/schema.ts (1)

82-84: Consider z.unknown() over z.any() for type safety.

z.any() disables type checking on the value side — consumers won't be prompted to narrow the type. z.unknown() retains the "accept anything" parsing behavior while producing a safer inferred type (Record<string, unknown> vs Record<string, any>). Based on learnings: "Use TypeScript with explicit types in all TypeScript files; avoid using any".

♻️ Suggested change
-  metadata: z.record(z.string(), z.any())
+  metadata: z.record(z.string(), z.unknown())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/schema.ts` around lines 82 - 84, The metadata schema
currently uses z.record(z.string(), z.any()) which yields unsafe types; change
the validator to z.record(z.string(), z.unknown()).optional().meta(...) so the
parser still accepts any value but the inferred TypeScript type becomes
Record<string, unknown> for better type safety; update any downstream usages
that relied on Record<string, any> to narrow/guard values as needed and run type
checks/tests to confirm no regressions.
packages/language-server/src/mcp/tools/query-by-metadata.ts (1)

104-121: Type the mode parameter with the match mode enum instead of string.

Both matches and getMatchedValue accept mode: string, but the actual domain is 'exact' | 'contains' | 'exists'. Using the inferred type from matchModeSchema (or a literal union) would catch misuse at compile time and enable exhaustive switch checking.

♻️ Suggested change
+type MatchMode = z.infer<typeof matchModeSchema>
+
-  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: string): boolean => {
+  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: MatchMode): boolean => {

Apply the same to getMatchedValue on line 127.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts` around lines 104
- 121, The functions matches and getMatchedValue currently accept mode: string
but should be typed to the specific union of match modes to enable compile-time
checks; change the parameter type in both matches and getMatchedValue to the
inferred type from matchModeSchema (or a literal union 'exact' | 'contains' |
'exists'), update their signatures accordingly, and remove/replace the default
switch case so the switch over mode is exhaustive (letting the compiler flag
missing cases).
packages/language-server/src/mcp/tools/query-outgoers-graph.ts (1)

120-124: Avoid Record<string, any> — use a typed intermediate.

The reshaping loop can be typed without any by using the output node shape or Omit/& on GraphNode.

♻️ Suggested change
-  const nodes: Record<string, any> = {}
+  const nodes: Record<string, Omit<typeof result.nodes[string], 'neighbors'> & { outgoers: typeof result.nodes[string]['neighbors'] }> = {}

Or more simply, define a local type alias. The key point is avoiding the any escape hatch. As per coding guidelines: "avoid using any".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.ts` around lines
120 - 124, The temporary `nodes` map is typed as `Record<string, any>` — replace
the `any` with a proper intermediate type derived from your GraphNode shape
(e.g. `type OutgoingNode = Omit<GraphNode, 'neighbors'> & { outgoers:
GraphNode['neighbors'] }`) and use that type for `nodes`; in the loop over
`result.nodes` keep the destructuring (`const { neighbors, ...rest } = node`)
and assign `nodes[id] = { ...rest, outgoers: neighbors }` ensuring `nodes` is
declared as `Record<string, OutgoingNode>` (or equivalent alias) instead of
`Record<string, any>`.
packages/language-server/src/mcp/tools/query-incomers-graph.ts (1)

120-124: Record<string, any> bypasses type safety.

The nodes variable is typed as Record<string, any>, losing the structure guaranteed by the output schema. Consider deriving a type from z.infer of the output schema or at minimum using the known shape.

Proposed fix
-  const nodes: Record<string, any> = {}
+  const nodes: Record<string, Omit<typeof result.nodes[string], 'neighbors'> & { incomers: typeof result.nodes[string]['neighbors'] }> = {}

Or more practically, use z.infer<typeof outputSchema>['nodes'] if accessible, or inline the shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts` around lines
120 - 124, The declaration const nodes: Record<string, any> = {} loses type
safety; change it to use the output schema's inferred type (e.g. const nodes:
z.infer<typeof outputSchema>['nodes'] = {} or create a specific interface
matching the schema) and ensure the loop assigns values compatible with that
type (referencing result.nodes, the nodes variable, and outputSchema to derive
the shape); this preserves typings for { neighbors, ...rest } and the resulting
incomers property.
packages/language-server/src/mcp/tools/query-graph.ts (1)

120-120: Type results array explicitly as ElementSummary[] to avoid any inference.

const results = [] infers as any[] in TypeScript. Since all pushed elements are ElementSummary (via serializeElement), type it explicitly as per the coding guideline: "Use TypeScript with explicit types in all TypeScript files; avoid using any".

Fix
-import { elementSummarySchema, projectIdSchema, serializeElement } from './_common'
+import { type ElementSummary, elementSummarySchema, projectIdSchema, serializeElement } from './_common'
-  const results = []
+  const results: ElementSummary[] = []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-graph.ts` at line 120, The
results array is inferred as any[]; explicitly type it as ElementSummary[] to
satisfy typing rules and avoid any inference—update the declaration of results
(used where serializeElement pushes values) to: const results: ElementSummary[]
= [] ensuring imports or type references to ElementSummary are available in this
module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Around line 176-215: The traversal can set truncated=true while some nodes
reference neighbors that were never added to the nodes map, causing callers to
get undefined for nodes[neighbor.elementId]; after the while loop in the
function that builds nodes (where variables nodes, neighbors, truncated, queue,
serializeElement are used) iterate over every nodes[elementId].neighbors and
filter out any neighbor whose elementId is not present in nodes (i.e., keep only
neighbors with nodes[neighbor.elementId] !== undefined) so callers like
query-incomers-graph/query-outgoers-graph never encounter dangling references;
do this filtering only when truncated is true (or always — either is acceptable)
to avoid returning neighbors that point to missing elements.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts`:
- Line 165: The code uses an unsafe cast when calling
serializeElement(deploymentElement as any); remove the cast and either (a)
implement a new serializeDeploymentElement function mirroring serializeElement's
logic and call that with DeploymentElementModel, or (b) extract a shared
interface (e.g., { id, name, kind, title, tags, getMetadata():..., views():...
}) and widen serializeElement's parameter to accept that interface, or (c) add
overloads for serializeElement to accept both ElementModel and
DeploymentElementModel; update the call site to pass deploymentElement without
any cast and adjust imports/types accordingly to satisfy TypeScript.

---

Duplicate comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Line 27: Replace the permissive z.any() in the metadata schema with
z.unknown() so inferred types don’t propagate any; specifically update the
metadata field declaration that uses z.record(z.string(), z.any()).optional() to
use z.record(z.string(), z.unknown()).optional(), keeping the describe call
intact (look for the metadata identifier and the z.record(...) usage in the
file).

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Line 199: The code is directly accessing rel.description.text which will throw
when description is null/undefined and is the wrong property for
MarkdownOrString; import and use flattenMarkdownOrString (or null-guard
manually) and replace the occurrences where description: rel.description.text is
set (search for the symbol rel.description.text in find-relationship-paths.ts)
with description: flattenMarkdownOrString(rel.description) (or description:
rel.description ? flattenMarkdownOrString(rel.description) : undefined) and add
the corresponding import for flattenMarkdownOrString at the top of the file.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Around line 102-104: The results array is silently capped at limit = 50;
update the output to indicate truncation like query-incomers-graph/query-graph:
compute the full match count (e.g., totalNodes or totalResults) before slicing,
set truncated = totalCount > limit, return truncated and the total count
alongside the sliced results, and update the output schema/type definitions and
the function (query-by-tags) return shape to include these new fields so callers
can detect truncation.
- Around line 125-131: The loop over model.deployment.elements() unsafely casts
each deploymentElement to any before calling serializeElement, which can pass
DeployedInstance (with kind?: never) and produce invalid output; change the loop
to filter/guard using the existing isDeploymentNode(deploymentElement) (or
equivalent type guard) and only call serializeElement on elements that pass
isDeploymentNode, keeping the matchesTags check and results.push call otherwise
unchanged.

In `@packages/language-server/src/mcp/tools/query-graph.spec.ts`:
- Around line 535-584: The test currently guards metadata/tags assertions with
"if (results.length > 0)" which lets an empty result silently pass; update the
assertions in the spec that calls queryGraph/handler so they explicitly assert
there is an incomer and validate its fields: assert results.length > 0 (or
toEqual(1)), then assert results[0].id === 'backend', assert results[0].tags
contains 'internal', and assert results[0].metadata.status === 'active' (and
still check tags is an Array and metadata is an Object); locate these changes
around the queryGraph invocation and the result handling in the "should include
metadata and tags in results" test.

---

Nitpick comments:
In `@apps/docs/src/content/docs/tooling/mcp.mdx`:
- Around line 158-173: The table of MCP commands mixes verb-led imperative
descriptions and bare noun phrases; normalize all descriptions to a consistent
verb-led present-tense style (e.g., "List all…", "Read full element details…",
"Search elements…", "Open the view…") by editing each row for commands like
`list-projects`, `read-project-summary`, `search-element`, `read-element`,
`read-deployment`, `read-view`, `find-relationships`, `query-graph`,
`query-incomers-graph`, `query-outgoers-graph`, `query-by-metadata`,
`query-by-tags`, `find-relationship-paths`, and `open-view` so every Description
cell starts with a verb and follows the same tense/voice for consistency and
scanability.

In `@packages/config/src/schema.ts`:
- Around line 82-84: The metadata schema currently uses z.record(z.string(),
z.any()) which yields unsafe types; change the validator to z.record(z.string(),
z.unknown()).optional().meta(...) so the parser still accepts any value but the
inferred TypeScript type becomes Record<string, unknown> for better type safety;
update any downstream usages that relied on Record<string, any> to narrow/guard
values as needed and run type checks/tests to confirm no regressions.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts`:
- Around line 104-121: The functions matches and getMatchedValue currently
accept mode: string but should be typed to the specific union of match modes to
enable compile-time checks; change the parameter type in both matches and
getMatchedValue to the inferred type from matchModeSchema (or a literal union
'exact' | 'contains' | 'exists'), update their signatures accordingly, and
remove/replace the default switch case so the switch over mode is exhaustive
(letting the compiler flag missing cases).

In `@packages/language-server/src/mcp/tools/query-graph.ts`:
- Line 120: The results array is inferred as any[]; explicitly type it as
ElementSummary[] to satisfy typing rules and avoid any inference—update the
declaration of results (used where serializeElement pushes values) to: const
results: ElementSummary[] = [] ensuring imports or type references to
ElementSummary are available in this module.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts`:
- Around line 120-124: The declaration const nodes: Record<string, any> = {}
loses type safety; change it to use the output schema's inferred type (e.g.
const nodes: z.infer<typeof outputSchema>['nodes'] = {} or create a specific
interface matching the schema) and ensure the loop assigns values compatible
with that type (referencing result.nodes, the nodes variable, and outputSchema
to derive the shape); this preserves typings for { neighbors, ...rest } and the
resulting incomers property.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.ts`:
- Around line 120-124: The temporary `nodes` map is typed as `Record<string,
any>` — replace the `any` with a proper intermediate type derived from your
GraphNode shape (e.g. `type OutgoingNode = Omit<GraphNode, 'neighbors'> & {
outgoers: GraphNode['neighbors'] }`) and use that type for `nodes`; in the loop
over `result.nodes` keep the destructuring (`const { neighbors, ...rest } =
node`) and assign `nodes[id] = { ...rest, outgoers: neighbors }` ensuring
`nodes` is declared as `Record<string, OutgoingNode>` (or equivalent alias)
instead of `Record<string, any>`.

Comment thread packages/language-server/src/mcp/tools/_common.ts
Comment thread packages/language-server/src/mcp/tools/query-by-metadata.ts Outdated
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
packages/language-server/src/mcp/tools/subgraph-summary.spec.ts (2)

30-34: Avoid any: replace {} as any and Array<any> with typed or unknown alternatives.

Every invocation passes {} as any as the second handler argument and casts structured-content fields to any or Array<any>. This defeats TypeScript's contract enforcement: if the handler signature or output schema changes, these tests will continue to compile and appear to pass even if the call is now wrong.

  • For the second argument, derive or import the MCP extra-context type and pass a properly typed stub (or use satisfies).
  • For data['descendants'] as Array<any> / data['root'] as any, prefer the actual output type emitted by the tool's Zod schema (or at minimum unknown with runtime narrowing).

Per coding guidelines: "Use TypeScript with explicit types in all TypeScript files; avoid using any."

♻️ Minimal improvement for a single test (pattern applies everywhere)
-    const [_name, _config, handler] = subgraphSummary(services.likec4.LanguageServices)
-    const result = await handler(
-      { elementId: 'parent', maxDepth: 10, metadataKeys: undefined, project: 'default' as ProjectId },
-      {} as any,
-    )
-    const root = data['root'] as any
-    const descendants = data['descendants'] as Array<any>
+    const [_name, _config, handler] = subgraphSummary(services.likec4.LanguageServices)
+    // Replace `{}` with a properly typed MCP extra-context stub once the type is importable
+    const result = await handler(
+      { elementId: 'parent', maxDepth: 10, metadataKeys: undefined, project: 'default' as ProjectId },
+      {} as Parameters<typeof handler>[1],
+    )
+    const root = result.structuredContent!['root'] as Record<string, unknown>
+    const descendants = result.structuredContent!['descendants'] as Array<Record<string, unknown>>

Also applies to: 82-86, 120-129, 161-165, 193-197, 223-227, 246-250, 279-283

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/subgraph-summary.spec.ts` around lines
30 - 34, Tests are using unsafe any casts; replace the {} as any second argument
to handler by importing or deriving the MCP extra-context type (the type used by
the handler) and pass a typed stub (or use "satisfies") when calling
subgraphSummary(...). Also change occurrences of data['descendants'] as
Array<any> and data['root'] as any to the real output types defined by the
tool's Zod schema (or at minimum use unknown and perform runtime narrowing) so
the test enforces the handler's contract; update all instances around
subgraphSummary, handler, data['descendants'], and data['root'] accordingly.

10-293: Missing test for the truncated = true path (MAX_RESULTS cap).

The tool caps results at MAX_RESULTS = 200 and sets truncated = true once that limit is hit. None of the seven tests exercise this branch, leaving the truncation signalling logic uncovered. Consider adding a test that populates more than 200 descendants (or temporarily patches the constant) and asserts truncated === true with totalDescendants still reflecting the full count.

Would you like me to draft a test case for the truncated = true branch, or open a new issue to track this gap?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/subgraph-summary.spec.ts` around lines
10 - 293, Add a new spec in subgraph-summary.spec.ts that triggers the
MAX_RESULTS truncation path: create a model under createTestServices() with more
than MAX_RESULTS descendants (or temporarily override the MAX_RESULTS constant
used by the subgraphSummary implementation), call the subgraphSummary(...)
handler for the root element, and assert that
result.structuredContent!['truncated'] === true while
result.structuredContent!['totalDescendants'] equals the full descendant count
and the returned descendants array length is capped at MAX_RESULTS; reference
the subgraphSummary handler, the MAX_RESULTS constant, createTestServices(), and
the result.structuredContent fields (descendants, totalDescendants, truncated)
when locating where to add the test.
packages/language-server/src/mcp/tools/find-relationship-paths.ts (1)

144-148: maxDepth is already clamped by zod schema — double clamping at line 146 is redundant but harmless.

The input schema at line 110 already enforces .min(1).max(5).default(3), so Math.min(args.maxDepth ?? 3, 5) at line 146 is a no-op. The ?? 3 fallback is also unreachable since .default(3) guarantees a value. Not a bug, but worth noting for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts` around
lines 144 - 148, The maxDepth value is redundantly clamped in the handler;
remove the unnecessary Math.min/?? fallback and use the validated value directly
(replace const maxDepth = Math.min(args.maxDepth ?? 3, 5) with const maxDepth =
args.maxDepth) because the input zod schema already enforces
.min(1).max(5).default(3); update any downstream uses of maxDepth to rely on
that variable.
packages/language-server/src/mcp/tools/query-by-tags.spec.ts (1)

38-42: Prefer typed alternatives over as any for handler context.

{} as any is used on lines 41, 72, 109, 139, 182, 214, 264, 293, 320, and 347 for the handler's second argument (the MCP request context). Consider defining a lightweight stub type (e.g., const emptyContext = {} as Pick<RequestHandlerExtra, never>) shared across test files to avoid scattering any casts. Similarly, as Array<any> (lines 76, 144, 219, 325) could be Array<unknown> when only length is asserted.

As per coding guidelines, "avoid using any" in all TypeScript files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-tags.spec.ts` around lines 38
- 42, The tests call the handler returned by queryByTags with a context cast as
`any` (e.g., `handler(..., {} as any)`) and use `as Array<any>` elsewhere;
replace these unsafe `any` casts by adding a lightweight reusable stub like
`const emptyContext = {} as Pick<RequestHandlerExtra, never>` and use
`emptyContext` as the second argument to the handler (or create a shared test
helper export), and where only array shape/length is asserted change
`Array<any>` to `Array<unknown>` (or `unknown[]`) to satisfy the linter while
keeping the tests minimal; update test files that call `queryByTags`/`handler`
and any other specs noted to use these typed alternatives.
packages/language-server/src/mcp/tools/query-by-metadata.ts (1)

99-121: Use the matchMode union type instead of string for the mode parameter.

Both matches (line 104) and getMatchedValue (line 126) accept mode: string instead of the narrower z.infer<typeof matchModeSchema> (i.e., 'exact' | 'contains' | 'exists'). Using the union type provides exhaustiveness checking and prevents silent fallthrough to the default/else branch.

Suggested change
-  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: string): boolean => {
+  type MatchMode = z.infer<typeof matchModeSchema>
+  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: MatchMode): boolean => {

Same for getMatchedValue at line 126.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts` around lines 99
- 121, The functions matches and getMatchedValue currently accept mode: string
which loses type safety; change their mode parameter types to the narrower union
by using z.infer<typeof matchModeSchema> (or an alias like type MatchMode =
z.infer<typeof matchModeSchema>) so mode is 'exact' | 'contains' | 'exists',
update their signatures (matches(..., mode: MatchMode) and getMatchedValue(...,
mode: MatchMode)) and then let TypeScript/zod enforce exhaustiveness in the
switch (remove or handle the default branch as needed).
packages/language-server/src/mcp/tools/_common.ts (1)

176-177: queue.shift() is O(n) per dequeue — consider a deque for large graphs.

Array.shift() is O(n) due to re-indexing, making the BFS loop O(n²) in the worst case. With maxNodes bounding the queue size, this is likely acceptable in practice, but if traversals grow larger, switching to an index-based approach (incrementing a head pointer) or a proper deque would help.

♻️ Lightweight index-based alternative
-  const queue: Array<{ elementId: string; depth: number }> = [{ elementId: startElementId, depth: 0 }]
+  const queue: Array<{ elementId: string; depth: number }> = [{ elementId: startElementId, depth: 0 }]
+  let queueHead = 0

-  while (queue.length > 0) {
-    const { elementId, depth } = queue.shift()!
+  while (queueHead < queue.length) {
+    const { elementId, depth } = queue[queueHead++]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/_common.ts` around lines 176 - 177,
The BFS uses Array.shift() (queue.shift()) which is O(n) per dequeue; replace it
with an index-based queue to avoid re-indexing: keep the existing array "queue"
but add a head pointer (e.g., head = 0) and dequeue by reading queue[head++] (or
use a Deque implementation) inside the BFS loop that references elementId and
depth; ensure you still respect the maxNodes bound and periodically compact the
array if head grows large to avoid unbounded memory use.
packages/language-server/src/mcp/tools/query-by-tags.ts (1)

104-106: Avoid any: type the results array and the deployment element cast.

Line 104: results lacks an explicit type annotation. Line 140: as any violates the project guideline to avoid any. Since serializeElement expects ElementModel<AnyAux>, consider narrowing the cast or typing more precisely.

♻️ Proposed fix
-  const results = []
+  const results: ElementSummary[] = []
-      results.push(serializeElement(deploymentElement as any))
+      results.push(serializeElement(deploymentElement as unknown as ElementModel<AnyAux>))
#!/bin/bash
# Check how other tools in the same directory handle the deployment element → serializeElement conversion
rg -n "serializeElement.*deployment" --type ts packages/language-server/src/mcp/tools/ -B2 -A2

Based on learnings: "Applies to **/*.{ts,tsx} : Use TypeScript with explicit types in all TypeScript files; avoid using any"

Also applies to: 140-140

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts` around lines 104 -
106, The results array is untyped and a deployment element is cast with `as
any`; change `results` to an explicit type (e.g., ResultElement[] or
ElementModel<SomeAux>[] depending on the actual element shape used in this file)
and replace the `as any` cast when passing the deployment to `serializeElement`
with a narrowed, specific type (e.g., `as ElementModel<SpecificAux>` or a
properly constructed ElementModel via a small mapper function) so
`serializeElement` receives `ElementModel<AnyAux>`-compatible input without
using `any`; update related declarations (`limit`, `truncated` can remain) and
import or declare the correct `ElementModel`/aux type to satisfy the signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/docs/src/content/docs/tooling/mcp.mdx`:
- Around line 168-169: The two table descriptions are inverted; update the MDX
table so `query-incomers-graph` matches its implementation and docs by changing
its description to "Recursive BFS graph of all upstream dependencies/producers"
and change `query-outgoers-graph` to "Recursive BFS graph of all downstream
consumers/dependents" (these correspond to the behavior documented in
query-incomers-graph.ts and query-outgoers-graph.ts).

In `@packages/language-server/src/mcp/tools/subgraph-summary.spec.ts`:
- Around line 252-255: The test currently uses a double non-null assertion
result.content![0]! without guarding for undefined; change it to first assert
that result.content is defined and has at least one item (e.g.,
expect(result.content).toBeDefined() and
expect(result.content!.length).toBeGreaterThan(0) or
expect(result.content?.[0]).toBeDefined()), then safely assign const content =
result.content![0] (single non-null) and perform the 'text' in content check —
this prevents a TypeError if content is empty or undefined while keeping the
intent of checking the error message.

---

Duplicate comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Around line 56-61: The current truthy guards using if (config.title) and if
(config.contactPerson) drop intentionally-set empty strings; update the checks
in the function that builds result to test for presence instead (e.g., use
config.title !== undefined or config.title != null and similarly for
config.contactPerson) so empty-string values are preserved when assigning
result.title and result.contactPerson.
- Line 27: Replace the use of z.any() in the metadata Zod schema with
z.unknown() to preserve validation while avoiding unsafe any usage: locate the
metadata definition (the record schema using z.record(z.string(),
z.any()).optional()) and change the inner type from z.any() to z.unknown(),
ensuring the rest of the schema and its .describe(...) call remain unchanged.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 196-202: The code reads rel.description.text which is invalid for
MarkdownOrString; import flattenMarkdownOrString from "@likec4/core/types" and
replace any use of rel.description.text with
flattenMarkdownOrString(rel.description) (and ensure you pass the possibly
null/undefined description through that helper) in the relationship construction
in find-relationship-paths (the block building relationship with
kind/title/description/technology/tags) and the other similar occurrence; this
fixes runtime undefined descriptions.

In `@packages/language-server/src/mcp/tools/query-by-metadata.spec.ts`:
- Around line 294-298: The test for queryByMetadata is still explicitly passing
matchMode: 'exact' which prevents exercising the default behavior; update the
test that calls the handler returned from
queryByMetadata(services.likec4.LanguageServices) so it omits the matchMode
property in the argument object (leave key, value, project as before) to assert
the default-to-exact path in the handler invoked by the test titled "should
default to exact match when matchMode not specified".

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts`:
- Line 165: The cast deploymentElement as any is hiding a typing mismatch—update
serializeElement in _common.ts to accept a structurally shared interface (e.g.,
ElementLike or ElementModelLike) that captures the properties used by
serializeElement, ensure that DeploymentNodeModel is compatible with that
interface (or export a shared interface used by both), then remove the ad-hoc
cast at the call site (the spread ...serializeElement(deploymentElement)).
Reference serializeElement, deploymentElement, DeploymentNodeModel and
_common.ts when making the change.

In `@packages/language-server/src/mcp/tools/query-by-tag-pattern.spec.ts`:
- Around line 233-262: The test name claims it checks truncation but never
triggers it because queryByTagPattern uses MAX_RESULTS = 50; update the spec in
query-by-tag-pattern.spec.ts to either rename the test to "should collect
matchedTagValues from multiple elements" (change the it() description) or modify
the fixture to create more than MAX_RESULTS matching elements (generate >50
elements with tags like 'sched_x') so the handler returned by
queryByTagPattern(services.likec4.LanguageServices) actually hits the truncation
branch and validates the truncated behavior for matchedTagValues.

In `@packages/language-server/src/mcp/tools/query-by-tag-pattern.ts`:
- Around line 136-138: The code is using a permissive "as any" cast on
deploymentElement when calling serializeElement; instead widen
serializeElement's signature in _common.ts to accept DeploymentNodeModel (or
more generally ElementModel<AnyAux> | DeploymentNodeModel) so the cast is
unnecessary. Update the serializeElement declaration in _common.ts to a broader
type that covers DeploymentNodeModel (and any structurally compatible models),
then remove the "as any" cast in query-by-tag-pattern.ts where
serializeElement(deploymentElement) is called and similar places (see
query-by-metadata.ts usage) so type compatibility is handled centrally.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.spec.ts`:
- Around line 34-36: Replace the repeated "{} as any" test argument with a
properly typed shared helper: add an exported constant like emptyTestContext (or
a createEmptyContext(): Context stub) in a test utilities module (e.g.,
tests/test-utils.ts) typed to the real context/interface expected by the
functions under test, then update all calls in query-incomers-graph.spec.ts
(where you pass the context argument to the function under test such as
queryIncomersGraph) to use emptyTestContext instead of "{} as any"; this removes
any casts, centralizes the stub, and keeps type safety across the occurrences
flagged in the file.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts`:
- Around line 120-132: When result.truncated is true, post-process the built
nodes map to remove any incomers that reference elementId values not present in
nodes so we don't return dangling references; after constructing nodes in
query-incomers-graph.ts (the nodes variable built from result.nodes and
neighbors -> incomers), filter each nodes[id].incomers array to keep only
entries whose elementId exists as a key in nodes (i.e., Object.hasOwnProperty or
Set of Object.keys(nodes)), mirroring the same truncation cleanup used in
query-outgoers-graph.ts to ensure consistency and no invalid cross-references
are returned.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.ts`:
- Around line 120-132: The outgoers array can contain elementId values that
point to nodes not present in the returned nodes map when result.truncated is
true; update the post-processing in the function that builds nodes (using
result, nodes, outgoers, GraphNodeNeighbor) to, when result.truncated is true,
filter or null-out any outgoer entries whose elementId is not a key in nodes
(e.g., replace with a sentinel, remove them, or add a flag) so callers doing
nodes[outgoer.elementId] no longer get undefined; ensure this logic mirrors the
fix applied in the incomers-graph handling and runs before returning the object.

In `@packages/mcp/README.md`:
- Around line 62-63: The README descriptions for the two queries are inverted:
update the two list items so `query-incomers-graph` is described as discovering
elements that point to the target (downstream consumers/dependents) and
`query-outgoers-graph` is described as discovering elements the source points at
(upstream dependencies/producers); locate the lines mentioning
`query-incomers-graph` and `query-outgoers-graph` and swap/replace their
descriptions to match the MDX docs (commit 36bd7a0) so wording and semantics
align.

---

Nitpick comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Around line 176-177: The BFS uses Array.shift() (queue.shift()) which is O(n)
per dequeue; replace it with an index-based queue to avoid re-indexing: keep the
existing array "queue" but add a head pointer (e.g., head = 0) and dequeue by
reading queue[head++] (or use a Deque implementation) inside the BFS loop that
references elementId and depth; ensure you still respect the maxNodes bound and
periodically compact the array if head grows large to avoid unbounded memory
use.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 144-148: The maxDepth value is redundantly clamped in the handler;
remove the unnecessary Math.min/?? fallback and use the validated value directly
(replace const maxDepth = Math.min(args.maxDepth ?? 3, 5) with const maxDepth =
args.maxDepth) because the input zod schema already enforces
.min(1).max(5).default(3); update any downstream uses of maxDepth to rely on
that variable.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts`:
- Around line 99-121: The functions matches and getMatchedValue currently accept
mode: string which loses type safety; change their mode parameter types to the
narrower union by using z.infer<typeof matchModeSchema> (or an alias like type
MatchMode = z.infer<typeof matchModeSchema>) so mode is 'exact' | 'contains' |
'exists', update their signatures (matches(..., mode: MatchMode) and
getMatchedValue(..., mode: MatchMode)) and then let TypeScript/zod enforce
exhaustiveness in the switch (remove or handle the default branch as needed).

In `@packages/language-server/src/mcp/tools/query-by-tags.spec.ts`:
- Around line 38-42: The tests call the handler returned by queryByTags with a
context cast as `any` (e.g., `handler(..., {} as any)`) and use `as Array<any>`
elsewhere; replace these unsafe `any` casts by adding a lightweight reusable
stub like `const emptyContext = {} as Pick<RequestHandlerExtra, never>` and use
`emptyContext` as the second argument to the handler (or create a shared test
helper export), and where only array shape/length is asserted change
`Array<any>` to `Array<unknown>` (or `unknown[]`) to satisfy the linter while
keeping the tests minimal; update test files that call `queryByTags`/`handler`
and any other specs noted to use these typed alternatives.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Around line 104-106: The results array is untyped and a deployment element is
cast with `as any`; change `results` to an explicit type (e.g., ResultElement[]
or ElementModel<SomeAux>[] depending on the actual element shape used in this
file) and replace the `as any` cast when passing the deployment to
`serializeElement` with a narrowed, specific type (e.g., `as
ElementModel<SpecificAux>` or a properly constructed ElementModel via a small
mapper function) so `serializeElement` receives
`ElementModel<AnyAux>`-compatible input without using `any`; update related
declarations (`limit`, `truncated` can remain) and import or declare the correct
`ElementModel`/aux type to satisfy the signature.

In `@packages/language-server/src/mcp/tools/subgraph-summary.spec.ts`:
- Around line 30-34: Tests are using unsafe any casts; replace the {} as any
second argument to handler by importing or deriving the MCP extra-context type
(the type used by the handler) and pass a typed stub (or use "satisfies") when
calling subgraphSummary(...). Also change occurrences of data['descendants'] as
Array<any> and data['root'] as any to the real output types defined by the
tool's Zod schema (or at minimum use unknown and perform runtime narrowing) so
the test enforces the handler's contract; update all instances around
subgraphSummary, handler, data['descendants'], and data['root'] accordingly.
- Around line 10-293: Add a new spec in subgraph-summary.spec.ts that triggers
the MAX_RESULTS truncation path: create a model under createTestServices() with
more than MAX_RESULTS descendants (or temporarily override the MAX_RESULTS
constant used by the subgraphSummary implementation), call the
subgraphSummary(...) handler for the root element, and assert that
result.structuredContent!['truncated'] === true while
result.structuredContent!['totalDescendants'] equals the full descendant count
and the returned descendants array length is capped at MAX_RESULTS; reference
the subgraphSummary handler, the MAX_RESULTS constant, createTestServices(), and
the result.structuredContent fields (descendants, totalDescendants, truncated)
when locating where to add the test.

Comment thread apps/docs/src/content/docs/tooling/mcp.mdx Outdated
Comment thread packages/language-server/src/mcp/tools/subgraph-summary.spec.ts
@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch from 20db12a to 220f5ab Compare February 19, 2026 13:55
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
packages/language-server/src/mcp/tools/query-by-tag-pattern.spec.ts (1)

61-100: Contains test omits matchedTagValues assertion present in other match-mode tests.

The prefix test (lines 55–57) explicitly asserts on matchedTagValues, but the equivalent contains test ends at line 98 without any matchedTagValues check. For consistency and fuller coverage, consider adding:

       expect(ids).toContain('a')
       expect(ids).toContain('b')
       expect(ids).not.toContain('c')
+      const matchedTagValues = result.structuredContent!['matchedTagValues'] as string[]
+      expect(matchedTagValues).toContain('target_asil_qm')
+      expect(matchedTagValues).toContain('unit_asil_qm')
+      expect(matchedTagValues).not.toContain('is_in_dag')
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-tag-pattern.spec.ts` around
lines 61 - 100, The contains-match test ("should match tags containing pattern
anywhere") is missing the assertion on matchedTagValues; after calling
queryByTagPattern and extracting results (via queryByTagPattern, handler,
result, results), add assertions that each matching result includes the expected
matchedTagValues (e.g., for id 'a' matchedTagValues contains 'target_asil_qm'
and for id 'b' contains 'unit_asil_qm') consistent with the prefix test pattern
so the test asserts both presence and the exact matched tag strings.
.changeset/mcp-query-tools-and-project-config-metadata.md (1)

7-9: Changeset description omits four new MCP tools.

batch-read-elements, subgraph-summary, element-diff, and query-by-tag-pattern are registered, tested, and public-facing tools introduced in this PR but absent from the changeset description. Consider updating the description to include them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/mcp-query-tools-and-project-config-metadata.md around lines 7 -
9, The changeset text is missing four newly added public MCP tools; update the
description to list all introduced tools by name so release notes are accurate:
add `batch-read-elements`, `subgraph-summary`, `element-diff`, and
`query-by-tag-pattern` alongside the already-mentioned `query-graph`,
`query-incomers-graph`, `query-outgoers-graph`, `query-by-metadata`,
`query-by-tags`, and `find-relationship-paths`, and mention the
`read-project-summary` enhancement and project config `metadata` schema change
to ensure the changeset fully documents the PR.
packages/config/src/schema.ts (1)

82-84: Use z.unknown() instead of z.any() for metadata values.

z.record(z.string(), z.any()) infers Record<string, any> which bypasses TypeScript's type system. z.unknown() preserves the open-ended nature while requiring callers to narrow before use.

♻️ Proposed fix
-  metadata: z.record(z.string(), z.any())
+  metadata: z.record(z.string(), z.unknown())
     .optional()
     .meta({ description: 'Arbitrary metadata as key-value pairs for custom project information' }),

As per coding guidelines: "Use TypeScript with explicit types in all TypeScript files; avoid using any."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/schema.ts` around lines 82 - 84, Replace the permissive
z.any() used in the metadata record with z.unknown() so the schema entry
metadata: z.record(z.string(), z.any()).optional().meta(...) becomes metadata:
z.record(z.string(), z.unknown()).optional().meta(...); this preserves
open-ended values while avoiding TypeScript any, keep the optional() and
meta(...) calls intact and update any related type expectations where metadata
is consumed to narrow unknown values before use.
packages/language-server/src/mcp/tools/batch-read-elements.spec.ts (1)

37-41: Pervasive as any and untyped assertions throughout the test file.

The handler's second argument is typed {} as any (lines 40, 80, 106, 138) and result arrays are cast to Array<any> / (e: any) throughout. This pattern repeats across all new spec files in this PR.

Consider defining a shared test helper or type alias for the handler context and result shapes to avoid any. For example:

♻️ Suggested approach
// In a shared test utilities file or at the top of the spec:
import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js'

// For the handler context arg, use the actual extra type or a minimal compatible shape:
const emptyExtra = {} as Parameters<ToolCallback>[1]

For result assertions, define inline shapes matching the tool's output schema rather than Array<any>.

As per coding guidelines: "Use TypeScript with explicit types in all TypeScript files; avoid using any."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/batch-read-elements.spec.ts` around
lines 37 - 41, Tests in batch-read-elements.spec.ts use pervasive `as any` for
the handler context and results (see batchReadElements and handler), which
violates TS typing rules; import the ToolCallback type (or the specific callback
type used by batchReadElements) and declare a shared typed helper (e.g.,
emptyExtra = {} as Parameters<ToolCallback>[1]) to pass as the second arg to
handler instead of {} as any, and define a small inline interface/type for the
expected result shape and cast the handler return to Array<ExpectedResult> (or
use ReturnType<ToolCallback> variants) so assertions use concrete types rather
than any.
packages/language-server/src/mcp/tools/query-by-metadata.ts (1)

104-121: mode parameter typed as string instead of the matchModeSchema inferred type.

Both matches (line 104) and getMatchedValue (line 127) accept mode: string, losing the exhaustiveness guarantee from the enum schema. Using the inferred type from the schema would make the default branch unreachable and catch future mode additions at compile time.

♻️ Suggested improvement
-  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: string): boolean => {
+  const matches = (metadataValue: string | string[], searchValue: string | undefined, mode: z.infer<typeof matchModeSchema>): boolean => {

Apply the same change to getMatchedValue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts` around lines 104
- 121, The functions matches and getMatchedValue currently accept mode: string
which loses exhaustiveness; change both function signatures to use the inferred
type from matchModeSchema (e.g. the schema's inferred/ReturnType) so TypeScript
knows the exact union of modes, update the switch in matches to rely on that
type (and remove or make the default unreachable) so adding new modes will
surface compile errors, and apply the same typed change to getMatchedValue to
preserve exhaustiveness and catch future mode additions at compile time.
packages/language-server/src/mcp/tools/find-relationship-paths.ts (1)

146-146: Redundant null-coalescing and clamping on an already-validated input.

args.maxDepth is guaranteed non-undefined (.default(3)) and ≤ 5 (.max(5)) by the input schema, so both ?? 3 and Math.min(..., 5) are dead code.

♻️ Proposed simplification
-  const maxDepth = Math.min(args.maxDepth ?? 3, 5)
+  const maxDepth = args.maxDepth
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts` at line
146, The current assignment uses redundant fallback and clamping: replace the
expression setting const maxDepth (currently using Math.min(args.maxDepth ?? 3,
5)) with a direct use of the validated input (const maxDepth = args.maxDepth)
because args.maxDepth is guaranteed non-undefined and already <= 5 by the input
schema; update the assignment in find-relationship-paths.ts where maxDepth is
declared to remove the unnecessary null-coalescing and Math.min wrapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/mcp-query-tools-and-project-config-metadata.md:
- Line 4: The changeset currently marks '@likec4/config' as a patch but you
added a backward-compatible feature (an optional metadata field) to
LikeC4ProjectJsonConfigSchema, so update the changeset entry from 'patch' to
'minor' for '@likec4/config' to reflect a minor semver bump; ensure the
changeset text references the same package name and the new optional metadata
addition.

In `@apps/docs/src/content/docs/tooling/mcp.mdx`:
- Around line 158-173: Add the four missing MCP tool rows to the "Available
tools" table: include `batch-read-elements` with a description like "Read full
details for multiple elements in a single request", `subgraph-summary` with
"Summarize a subgraph (nodes/edges/metrics) for a given root element or view",
`element-diff` with "Compute and display differences between two element
versions or instances", and `query-by-tag-pattern` with "Search elements by tag
name patterns/regex (supports wildcards and partial matches)"; place these rows
alongside the existing tool entries (e.g., near `query-by-tags`) so they appear
in the same table and follow the formatting of the other tool rows.

In `@packages/mcp/README.md`:
- Around line 61-66: The README's "Available tools" list and corresponding MDX
table are missing four newly registered tools: batch-read-elements,
subgraph-summary, element-diff, and query-by-tag-pattern; update the list and
table to include each tool with a one-line description that matches their
implementation (e.g., batch-read-elements: bulk fetch elements by id list;
subgraph-summary: produce summary stats for a subgraph; element-diff: show
differences between two element versions; query-by-tag-pattern: search elements
by tag pattern matching), ensuring the exact tool names (batch-read-elements,
subgraph-summary, element-diff, query-by-tag-pattern) are added so the docs
reflect the user-callable tools introduced in the PR.

---

Duplicate comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Line 27: The metadata property in projectConfigSchema currently uses z.any(),
which yields an unsafe Record<string, any>; change the schema to use z.unknown()
instead (i.e., update the metadata line in projectConfigSchema to
z.record(z.string(), z.unknown()).optional()) so metadata values are typed as
unknown rather than any, preserving safety while keeping the same structure.
- Around line 56-64: The truthy guards on config.title, config.contactPerson,
and config.metadata (used when assigning to result.title, result.contactPerson,
result.metadata) drop intentionally set empty-string values; change each check
from a truthy test (e.g., if (config.title)) to an explicit undefined check
(e.g., if (config.title !== undefined)) so empty strings are preserved—follow
the same pattern already used for manualLayouts.outDir with `??` semantics.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.spec.ts`:
- Around line 421-454: The describe block title "default maxDepth" is misleading
because the test calls findRelationshipPaths.handler with maxDepth: 3
explicitly; either remove the explicit maxDepth parameter from the handler call
to let the schema default be exercised (invoke handler with { sourceId: 'a',
targetId: 'e', includeIndirect: false, project: 'default' as ProjectId }
instead), or update the describe/it titles to reflect that this is testing an
explicit maxDepth boundary (e.g., rename the describe to 'maxDepth boundary' or
the it to 'should not find paths beyond maxDepth=3'); changes should be made
around the test that calls
findRelationshipPaths(services.likec4.LanguageServices) and invokes handler with
the request object.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 196-203: Replace the unsafe `.text` access on rel.description with
the canonical flattener: import flattenMarkdownOrString from '@likec4/core' at
the top of the file and use flattenMarkdownOrString(rel.description) in both
PathStep construction sites (the objects building relationship with
kind/title/description) so description becomes the flattened string or null;
update the two occurrences that currently read rel.description.text to call
flattenMarkdownOrString(rel.description) instead.

In `@packages/language-server/src/mcp/tools/query-by-metadata.spec.ts`:
- Around line 277-304: The test claims to verify the default matchMode but
explicitly passes matchMode: 'exact'; remove the matchMode property from the
handler call so the request relies on the schema default, i.e., call handler
with { key: 'owner', value: 'platform-team', project: 'default' as ProjectId }
(keep the surrounding setup and assertions unchanged) to ensure
queryByMetadata/handler uses the default matchMode.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts`:
- Line 165: Remove the lingering "as any" cast on deploymentElement and fix the
typing mismatch: either write a small converter or type guard that maps a
DeploymentElementModel (DeploymentFqn) to the expected ElementModel (Fqn) shape,
or adjust serializeElement's signature to accept DeploymentElementModel |
ElementModel; update the call site where deploymentElement is passed to
serializeElement to use that converter/guard or the widened signature so the
explicit "as any" cast is no longer needed and types align (refer to symbols:
deploymentElement, serializeElement, DeploymentElementModel, ElementModel,
DeploymentFqn, Fqn).

In `@packages/language-server/src/mcp/tools/query-by-tag-pattern.spec.ts`:
- Around line 233-262: The test "should collect all matchedTagValues even when
truncated" doesn't trigger the truncation branch and lacks an assertion for the
truncated flag; update the test that calls queryByTagPattern (use the returned
handler from queryByTagPattern(services.likec4.LanguageServices)) so it either
(A) creates more than MAX_RESULTS distinct tagged elements to force
results.length >= MAX_RESULTS and assert that the handler
response.structuredContent['truncated'] is true and that matchedTagValues
contains expected values, or (B) if you intend to test the non-truncated path,
rename the test to reflect non-truncation and add an assertion that
response.structuredContent['truncated'] is false while still asserting
matchedTagValues contains 'sched_a' and 'sched_b'; reference MAX_RESULTS,
queryByTagPattern, handler, matchedTagValues, and truncated when making the
change.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts`:
- Around line 1-3: This file contains a duplicated NVIDIA copyright/header block
(the lines starting with "SPDX-License-Identifier: MIT" and the "Copyright (c)
2026 NVIDIA CORPORATION & AFFILIATES" line); remove the redundant copy so only a
single canonical header remains at the top of the file (ensure the
SPDX-License-Identifier and the NVIDIA copyright line are present once), and run
a quick scan of other files in the same module to ensure no other duplicate
header instances remain.

In `@packages/language-server/src/mcp/tools/read-project-summary.ts`:
- Line 165: Remove the unnecessary optional chaining and fallback around
project.documents: replace the expression using project.documents?.map(...) ??
[] with a direct call to project.documents.map(d => d.fsPath). Rely on the fact
that project.documents is a ReadonlyArray<URI> and always defined; update the
object property (sources) where the current code uses project.documents?.map and
the ?? [] fallback so it simply maps project.documents to fsPath.

In `@packages/mcp/README.md`:
- Around line 62-63: The README descriptions for the commands are inverted: swap
the explanations for `query-incomers-graph` and `query-outgoers-graph` so
`query-incomers-graph` describes getting the complete graph of downstream
consumers/dependents (recursive incomers) and `query-outgoers-graph` describes
getting the complete graph of upstream dependencies/producers (recursive
outgoers); update the two bullet lines accordingly to match the MDX fix (commit
36bd7a0).

---

Nitpick comments:
In @.changeset/mcp-query-tools-and-project-config-metadata.md:
- Around line 7-9: The changeset text is missing four newly added public MCP
tools; update the description to list all introduced tools by name so release
notes are accurate: add `batch-read-elements`, `subgraph-summary`,
`element-diff`, and `query-by-tag-pattern` alongside the already-mentioned
`query-graph`, `query-incomers-graph`, `query-outgoers-graph`,
`query-by-metadata`, `query-by-tags`, and `find-relationship-paths`, and mention
the `read-project-summary` enhancement and project config `metadata` schema
change to ensure the changeset fully documents the PR.

In `@packages/config/src/schema.ts`:
- Around line 82-84: Replace the permissive z.any() used in the metadata record
with z.unknown() so the schema entry metadata: z.record(z.string(),
z.any()).optional().meta(...) becomes metadata: z.record(z.string(),
z.unknown()).optional().meta(...); this preserves open-ended values while
avoiding TypeScript any, keep the optional() and meta(...) calls intact and
update any related type expectations where metadata is consumed to narrow
unknown values before use.

In `@packages/language-server/src/mcp/tools/batch-read-elements.spec.ts`:
- Around line 37-41: Tests in batch-read-elements.spec.ts use pervasive `as any`
for the handler context and results (see batchReadElements and handler), which
violates TS typing rules; import the ToolCallback type (or the specific callback
type used by batchReadElements) and declare a shared typed helper (e.g.,
emptyExtra = {} as Parameters<ToolCallback>[1]) to pass as the second arg to
handler instead of {} as any, and define a small inline interface/type for the
expected result shape and cast the handler return to Array<ExpectedResult> (or
use ReturnType<ToolCallback> variants) so assertions use concrete types rather
than any.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Line 146: The current assignment uses redundant fallback and clamping: replace
the expression setting const maxDepth (currently using Math.min(args.maxDepth ??
3, 5)) with a direct use of the validated input (const maxDepth = args.maxDepth)
because args.maxDepth is guaranteed non-undefined and already <= 5 by the input
schema; update the assignment in find-relationship-paths.ts where maxDepth is
declared to remove the unnecessary null-coalescing and Math.min wrapping.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts`:
- Around line 104-121: The functions matches and getMatchedValue currently
accept mode: string which loses exhaustiveness; change both function signatures
to use the inferred type from matchModeSchema (e.g. the schema's
inferred/ReturnType) so TypeScript knows the exact union of modes, update the
switch in matches to rely on that type (and remove or make the default
unreachable) so adding new modes will surface compile errors, and apply the same
typed change to getMatchedValue to preserve exhaustiveness and catch future mode
additions at compile time.

In `@packages/language-server/src/mcp/tools/query-by-tag-pattern.spec.ts`:
- Around line 61-100: The contains-match test ("should match tags containing
pattern anywhere") is missing the assertion on matchedTagValues; after calling
queryByTagPattern and extracting results (via queryByTagPattern, handler,
result, results), add assertions that each matching result includes the expected
matchedTagValues (e.g., for id 'a' matchedTagValues contains 'target_asil_qm'
and for id 'b' contains 'unit_asil_qm') consistent with the prefix test pattern
so the test asserts both presence and the exact matched tag strings.

Comment thread .changeset/mcp-query-tools-and-project-config-metadata.md Outdated
Comment thread apps/docs/src/content/docs/tooling/mcp.mdx
Comment thread packages/mcp/README.md
@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch from 220f5ab to 38f31d5 Compare February 19, 2026 14:10
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts (1)

362-366: Consider extracting a helper to reduce as any boilerplate.

The pattern of destructuring [_name, _config, handler], passing {} as any as the extra context, and casting structuredContent fields with as any is repeated 15 times. A small helper would reduce noise and could improve type safety:

Example helper extraction
// At top of describe block:
type ToolFn = (...args: any[]) => [string, unknown, (params: any, extra: any) => Promise<any>]
const EMPTY_EXTRA = {} as never

async function callTool<P>(toolFn: ToolFn, params: P) {
  const [, , handler] = toolFn()
  const result = await handler(params, EMPTY_EXTRA)
  expect(result.structuredContent).toBeDefined()
  return result.structuredContent! as Record<string, unknown>
}

This would also reduce the as any footprint, aligning better with the guideline to avoid any. As per coding guidelines, "Use TypeScript with explicit types in all TypeScript files; avoid using any".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts`
around lines 362 - 366, Extract a small typed helper to replace the repeated
pattern around subgraphSummary(services.likec4.LanguageServices) destructuring
and the "{} as any" extra param: create a ToolFn type, an EMPTY_EXTRA constant
(e.g., {} as never) and a generic callTool<P>(toolFn, params) that invokes the
third tuple element (handler) with params and EMPTY_EXTRA, asserts
result.structuredContent exists and returns it typed (Record<string, unknown>),
then update all tests to call callTool(subgraphSummary(...), params) instead of
manually destructuring and passing "{} as any" or casting structuredContent.
packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts (3)

46-58: consumer1 node is never asserted

The test verifies source, consumer2, and consumer3, but consumer1 — a direct outgoer of source at depth 1 — has no assertions on its presence, depth, or outgoers count. A shallow check keeps the test complete.

✏️ Suggested addition
    expect(nodes['consumer3']!.depth).toBe(2)
    expect(nodes['consumer3']!.outgoers).toHaveLength(0)
+
+   expect(nodes['consumer1']).toBeDefined()
+   expect(nodes['consumer1']!.depth).toBe(1)
+   expect(nodes['consumer1']!.outgoers).toHaveLength(0)
  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 46 - 58, Add assertions for the missing consumer1 node: verify
nodes['consumer1'] is defined, that nodes['consumer1']!.depth equals 1, and that
nodes['consumer1']!.outgoers has the expected length (likely 0 if it has no
children) and optionally that its outgoers entries have an 'elementId' property
when non-empty; update the test alongside the other node checks (e.g., near the
checks for 'consumer2' and 'consumer3') using the same pattern.

10-247: No test exercises includeIndirect: false

Every test passes includeIndirect: true. The parameter is described as "Include relationships through nested elements", so its false branch (direct relationships only) has a meaningfully different traversal path that is entirely uncovered.

✏️ Suggested additional test
+  it('should exclude indirect relationships when includeIndirect is false', async () => {
+    const { validate, buildLikeC4Model, services } = createTestServices()
+
+    await validate(`
+      specification {
+        element system
+        element service
+      }
+      model {
+        source = system {
+          api = service
+        }
+        target = system
+
+        source.api -> target
+      }
+    `)
+
+    await buildLikeC4Model()
+
+    const [, , handler] = queryOutgoersGraph(services.likec4.LanguageServices)
+
+    const indirect = await handler(
+      { elementId: 'source', includeIndirect: true, maxDepth: 50, maxNodes: 1000, project: 'default' as ProjectId },
+      {} as any,
+    )
+    const direct = await handler(
+      { elementId: 'source', includeIndirect: false, maxDepth: 50, maxNodes: 1000, project: 'default' as ProjectId },
+      {} as any,
+    )
+
+    // With indirect=true 'target' is reachable via source.api; with false it should not be
+    const indirectNodes = indirect.structuredContent!['nodes'] as Record<string, unknown>
+    const directNodes = direct.structuredContent!['nodes'] as Record<string, unknown>
+    expect(indirectNodes['target']).toBeDefined()
+    expect(directNodes['target']).toBeUndefined()
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 10 - 247, Add a unit test in the query-outgoers-graph suite that calls the
handler returned by queryOutgoersGraph(services.likec4.LanguageServices) with
includeIndirect: false and a small model containing nested elements and
multi-level relationships; assert that only direct outgoers (depth 0 -> 1) are
returned, totalNodes equals the direct targets count, maxDepth is 1, and that
indirect targets (depth >1 or nested children) are excluded; use existing
patterns in other tests (createTestServices, validate, buildLikeC4Model, and the
handler invocation) and mirror assertions on result.structuredContent,
content['nodes'], content['totalNodes'], content['maxDepth'], and
content['truncated'] as appropriate.

87-88: Cycle test assertions are too sparse

The cycle test only checks totalNodes. It does not verify truncated: false, confirm all three nodes (a, b, c) are present in the result, or check that depths are bounded. A minimal expansion guards against a future regression where the cycle is detected but the returned shape is wrong.

✏️ Suggested additions
    expect(result.structuredContent).toBeDefined()
    expect(result.structuredContent!['totalNodes']).toBe(3)
+   expect(result.structuredContent!['truncated']).toBe(false)
+   const cycleNodes = result.structuredContent!['nodes'] as Record<string, unknown>
+   expect(cycleNodes['a']).toBeDefined()
+   expect(cycleNodes['b']).toBeDefined()
+   expect(cycleNodes['c']).toBeDefined()
  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 87 - 88, Add stronger assertions to the cycle test: after ensuring
result.structuredContent and result.structuredContent!['totalNodes'] === 3, also
assert result.structuredContent!['truncated'] === false, verify the returned
node list contains nodes with ids 'a', 'b' and 'c' (e.g. inspect
result.structuredContent!['nodes'] or similar key), and add a check that each
node's depth property is within an expected bound (e.g. depth is a finite number
and <= a small max like 2) to guard against incorrect shapes or unbounded
depths.
packages/language-server/src/mcp/tools/query-by-metadata.ts (1)

124-142: Consider using switch in getMatchedValue to match the pattern used in matches.

The matches function (line 108) uses a clean switch (mode) statement, but getMatchedValue uses an if-else chain for the same discriminant. Aligning them improves readability and consistency.

♻️ Suggested refactor
   const getMatchedValue = (
     metadataValue: string | string[],
     searchValue: string | undefined,
     mode: MatchMode,
   ): string => {
     const values = Array.isArray(metadataValue) ? metadataValue : [metadataValue]
 
-    if (mode === 'exists' || searchValue === undefined) {
-      return values[0] || ''
-    }
-    if (mode === 'exact') {
-      return values.find(v => v === searchValue) || values[0] || ''
-    }
-    if (mode === 'contains') {
-      const searchLower = searchValue.toLowerCase()
-      return values.find(v => v.toLowerCase().includes(searchLower)) || values[0] || ''
+    switch (mode) {
+      case 'exists':
+        return values[0] || ''
+      case 'exact':
+        if (searchValue === undefined) return values[0] || ''
+        return values.find(v => v === searchValue) || values[0] || ''
+      case 'contains': {
+        if (searchValue === undefined) return values[0] || ''
+        const searchLower = searchValue.toLowerCase()
+        return values.find(v => v.toLowerCase().includes(searchLower)) || values[0] || ''
+      }
+      default:
+        return values[0] || ''
     }
-    return values[0] || ''
   }

As per coding guidelines: "Favor switch(true) over if-else chains for complex conditional logic."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts` around lines 124
- 142, The getMatchedValue function uses an if-else chain to discriminate on
MatchMode; replace that chain with a switch(mode) (matching the pattern used in
matches) to improve readability and consistency: keep the same behavior for
cases 'exists' and when searchValue is undefined (return values[0] || ''),
handle 'exact' by returning values.find(v => v === searchValue) || values[0] ||
'', handle 'contains' by computing searchLower and returning values.find(v =>
v.toLowerCase().includes(searchLower)) || values[0] || '', and include a default
that returns values[0] || ''. Ensure you reference the existing parameters and
preserve array-coercion logic at the top of getMatchedValue.
packages/config/src/schema.ts (1)

82-84: Prefer z.unknown() over z.any() for type safety and consistency.

The metadata field uses z.any() which infers as Record<string, any> in TypeScript. The corresponding projectConfigSchema in _common.ts (line 34) already uses z.record(z.string(), z.unknown()) for the same metadata concept. Using z.unknown() here would be consistent and avoids leaking any into downstream types.

♻️ Suggested fix
-  metadata: z.record(z.string(), z.any())
+  metadata: z.record(z.string(), z.unknown())
     .optional()
     .meta({ description: 'Arbitrary metadata as key-value pairs for custom project information' }),

As per coding guidelines: "avoid using any" in TypeScript files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/schema.ts` around lines 82 - 84, Update the metadata
record schema to use z.unknown() instead of z.any() to avoid leaking any into
generated types and match the existing projectConfigSchema in _common.ts;
specifically change the metadata field declaration (the metadata:
z.record(z.string(), z.any()).optional().meta(... ) entry) to use
z.record(z.string(), z.unknown()) while keeping .optional() and .meta(...)
intact so runtime behavior and descriptions remain the same.
packages/language-server/src/mcp/tools/subgraph-summary.ts (1)

145-183: BFS traverses the full subtree after truncated = true solely to count totalDescendants.

After the 200-result cap is hit, every remaining element is still dequeued, counted, and has its children enqueued. For deep hierarchies (maxDepth up to 20) with thousands of nodes, this can be expensive. If an exact post-truncation count isn't required by callers, consider stopping child-enqueuing once truncated = true and replacing totalDescendants with a lower-bound note, or at minimum documenting that this field reflects the full BFS count and not just the returned slice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/subgraph-summary.ts` around lines 145
- 183, The loop in the BFS (the while(queue.length > 0) block) continues to
dequeue nodes and enqueue their children even after truncated is set when
descendants reaches MAX_RESULTS, causing expensive full traversal while only a
slice is returned; modify the loop so that once truncated is true you still may
increment totalDescendants if needed but do not call element.children() /
enqueue children (i.e., skip the for (const child of children) queue.push(...)
and avoid building children array after truncated), or alternatively stop
counting further nodes and document totalDescendants as a lower bound; update
references to truncated, totalDescendants, MAX_RESULTS, descendants,
element.children(), and the BFS loop to implement this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts`:
- Around line 217-231: The metadata key "bandwith" is misspelled in the test
fixtures; update the three occurrences to "bandwidth" inside the metadata blocks
for the interactions labeled dashboard -> cloud.next.graphql.updateAccount
("mutates via GraphQL") and dashboard -> cloud.legacy.backend.services ("fetches
via REST") as well as the earlier metadata block so the keys read bandwidth
'medium' / 'low' / 'high' respectively; ensure you adjust every instance of
bandwith to bandwidth to avoid future metadata-based test breakage.

In `@packages/language-server/src/mcp/tools/query-by-tags.ts`:
- Line 104: The local variable results is declared without an explicit type;
update the declaration in query-by-tags.ts (the const results = [] line) to use
an explicit array type that matches the items being pushed (e.g., const results:
<ItemType>[] = []), where <ItemType> should be the concrete interface/type used
by this module (or the function's return element type such as SearchResult,
TagQueryResult, etc.); do not use any—pick the existing result type used
elsewhere in this file or export a new appropriate interface and apply it to
results.
- Around line 122-143: The outer guard `if (results.length < limit)` causes
deployment elements to be skipped when `results.length === limit`, leaving
`truncated` false even though more elements may exist; remove that outer guard
so the deployment loop runs unconditionally and rely on the existing inner check
(`if (results.length >= limit) { truncated = true; break }`) to set `truncated`
and stop when the limit is reached; update the block that iterates `for (const
deploymentElement of model.deployment.elements())` (and keep the
`isDeploymentNodeModel`, `matchesTags`, and `serializeElement` checks) so any
skipped extra elements correctly set `truncated` when halted.

---

Duplicate comments:
In `@apps/docs/src/content/docs/tooling/mcp.mdx`:
- Around line 156-174: The table of available tools is missing four registered
MCP tools; add entries for `batch-read-elements`, `subgraph-summary`,
`element-diff`, and `query-by-tag-pattern` to the "Available tools" table in the
docs to match the registrations in MCPServerFactory.ts (where they are added
around the register calls). For each tool row, include a concise one-line
description consistent with the style of the other rows (Tool name in backticks
and a short Description) so the doc and the MCPServerFactory.ts registrations
stay in sync.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.spec.ts`:
- Around line 421-454: The test claims to verify the "default maxDepth" but the
handler call to findRelationshipPaths currently passes an explicit maxDepth: 3;
to exercise the schema default remove the maxDepth property from the request
object passed to the handler (the call where you build the request with {
sourceId: 'a', targetId: 'e', maxDepth: 3, ... }), leaving other fields
(includeIndirect, project) intact so the handler uses its default maxDepth;
alternatively, if you intended to assert behavior for an explicit depth, rename
the describe block to reflect that instead of changing the request.
- Around line 28-34: The tests use broad unsafe casts like "{} as any" for the
handler context and "any[]" for path steps; replace these with proper types:
pass a correctly typed HandlerContext (or the specific context interface used by
the handler function) instead of "{} as any" when calling handler, and declare
small inline interfaces (e.g., interface Step { /* id, type, ... */ } and
interface Path { length: number; steps: Step[] }) to type
result.structuredContent['paths'] as Path[]; update all similar assertions in
find-relationship-paths.spec.ts to use these explicit types rather than
any/any[] so the test adheres to project type guidelines and improves
compile-time checking.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 196-202: The code accesses a non-existent .text on
MarkdownOrString (rel.description.text) causing runtime errors; import
flattenMarkdownOrString from '@likec4/core/types' and replace uses of
rel.description.text with flattenMarkdownOrString(rel.description) (ensuring it
handles null/undefined) wherever rel.description is read in
find-relationship-paths.ts (e.g., the relationship object construction and the
other occurrence that mirror this pattern).

In `@packages/language-server/src/mcp/tools/query-incomers-graph.spec.ts`:
- Line 35: Replace every occurrence of "{} as any" used as the second argument
to the handler invocations with the proper MCP context type instead of any;
identify the six handler calls in this test (the invocations passing "{} as
any") and change that parameter to the concrete type that ToolCallback expects
(for example RequestHandlerExtra from the MCP SDK or the exact type
alias/Interface that ToolCallback's second parameter resolves to) so the tests
use the correct typed context rather than any.

In `@packages/language-server/src/mcp/tools/query-incomers-graph.ts`:
- Around line 120-132: The returned graph can contain dangling incomers
referencing elementIds not present in the returned nodes when result.truncated
is true; update the post-processing in query-incomers-graph (the block building
the local nodes map from result.nodes) to, if result.truncated is true, replace
each node's incomers array with a filtered version that only keeps incomers
whose elementId exists as a key in the nodes map (use the same ids used when
populating nodes) so callers resolving nodes[incomer.elementId] won't get
undefined; leave incomers unchanged when not truncated.
- Around line 1-3: This file contains a duplicated or non-standard copyright
header; remove the extra header so only the project-standard SPDX and copyright
header remains at the top of the file, ensure it matches the canonical template
(SPDX: MIT and "Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights
reserved."), and keep a single blank line after the header; check the top
comment block in query-incomers-graph.ts and replace it with the standard header
if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts`:
- Around line 1-3: The file currently contains a duplicated NVIDIA copyright
header and SPDX line (the SPDX identifier and the multi-line copyright block in
the file head); decide whether to retain, update, or remove it and then apply
that decision consistently: either remove the duplicated header lines, or
replace them with the agreed canonical header (ensure the SPDX identifier
matches project policy and the copyright owner text is correct), and run a
repo-wide check to ensure other files use the same canonical header format so
this file (packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts)
matches the maintained standard.

In `@packages/language-server/src/mcp/tools/read-project-summary.ts`:
- Around line 162-166: The optional chaining on project.documents is unnecessary
because documents is guaranteed to be a ReadonlyArray<URI>; update the object
returned in read-project-summary (the entry constructing
title/folder/sources/config) to call project.documents.map(d => d.fsPath)
instead of project.documents?.map(...) so sources is a plain array, leaving
serializeConfig(project.config) and the projectConfigSchema usage unchanged.

In `@packages/mcp/README.md`:
- Around line 61-66: The README's tool list omits four tools that are registered
in MCPServerFactory; update the list to include `batch-read-elements`,
`subgraph-summary`, `element-diff`, and `query-by-tag-pattern` with short
descriptions consistent with the existing entries (e.g., batch element
retrieval, subgraph summarization, element difference comparison, and
tag-pattern search). Locate the tool registry in MCPServerFactory to copy or
paraphrase the authoritative descriptions and add them into the README's tool
list so the docs and MCPServerFactory remain in sync.

---

Nitpick comments:
In `@packages/config/src/schema.ts`:
- Around line 82-84: Update the metadata record schema to use z.unknown()
instead of z.any() to avoid leaking any into generated types and match the
existing projectConfigSchema in _common.ts; specifically change the metadata
field declaration (the metadata: z.record(z.string(),
z.any()).optional().meta(... ) entry) to use z.record(z.string(), z.unknown())
while keeping .optional() and .meta(...) intact so runtime behavior and
descriptions remain the same.

In `@packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts`:
- Around line 362-366: Extract a small typed helper to replace the repeated
pattern around subgraphSummary(services.likec4.LanguageServices) destructuring
and the "{} as any" extra param: create a ToolFn type, an EMPTY_EXTRA constant
(e.g., {} as never) and a generic callTool<P>(toolFn, params) that invokes the
third tuple element (handler) with params and EMPTY_EXTRA, asserts
result.structuredContent exists and returns it typed (Record<string, unknown>),
then update all tests to call callTool(subgraphSummary(...), params) instead of
manually destructuring and passing "{} as any" or casting structuredContent.

In `@packages/language-server/src/mcp/tools/query-by-metadata.ts`:
- Around line 124-142: The getMatchedValue function uses an if-else chain to
discriminate on MatchMode; replace that chain with a switch(mode) (matching the
pattern used in matches) to improve readability and consistency: keep the same
behavior for cases 'exists' and when searchValue is undefined (return values[0]
|| ''), handle 'exact' by returning values.find(v => v === searchValue) ||
values[0] || '', handle 'contains' by computing searchLower and returning
values.find(v => v.toLowerCase().includes(searchLower)) || values[0] || '', and
include a default that returns values[0] || ''. Ensure you reference the
existing parameters and preserve array-coercion logic at the top of
getMatchedValue.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts`:
- Around line 46-58: Add assertions for the missing consumer1 node: verify
nodes['consumer1'] is defined, that nodes['consumer1']!.depth equals 1, and that
nodes['consumer1']!.outgoers has the expected length (likely 0 if it has no
children) and optionally that its outgoers entries have an 'elementId' property
when non-empty; update the test alongside the other node checks (e.g., near the
checks for 'consumer2' and 'consumer3') using the same pattern.
- Around line 10-247: Add a unit test in the query-outgoers-graph suite that
calls the handler returned by
queryOutgoersGraph(services.likec4.LanguageServices) with includeIndirect: false
and a small model containing nested elements and multi-level relationships;
assert that only direct outgoers (depth 0 -> 1) are returned, totalNodes equals
the direct targets count, maxDepth is 1, and that indirect targets (depth >1 or
nested children) are excluded; use existing patterns in other tests
(createTestServices, validate, buildLikeC4Model, and the handler invocation) and
mirror assertions on result.structuredContent, content['nodes'],
content['totalNodes'], content['maxDepth'], and content['truncated'] as
appropriate.
- Around line 87-88: Add stronger assertions to the cycle test: after ensuring
result.structuredContent and result.structuredContent!['totalNodes'] === 3, also
assert result.structuredContent!['truncated'] === false, verify the returned
node list contains nodes with ids 'a', 'b' and 'c' (e.g. inspect
result.structuredContent!['nodes'] or similar key), and add a check that each
node's depth property is within an expected bound (e.g. depth is a finite number
and <= a small max like 2) to guard against incorrect shapes or unbounded
depths.

In `@packages/language-server/src/mcp/tools/subgraph-summary.ts`:
- Around line 145-183: The loop in the BFS (the while(queue.length > 0) block)
continues to dequeue nodes and enqueue their children even after truncated is
set when descendants reaches MAX_RESULTS, causing expensive full traversal while
only a slice is returned; modify the loop so that once truncated is true you
still may increment totalDescendants if needed but do not call
element.children() / enqueue children (i.e., skip the for (const child of
children) queue.push(...) and avoid building children array after truncated), or
alternatively stop counting further nodes and document totalDescendants as a
lower bound; update references to truncated, totalDescendants, MAX_RESULTS,
descendants, element.children(), and the BFS loop to implement this change.

Comment thread packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts Outdated
Comment thread packages/language-server/src/mcp/tools/query-by-tags.ts Outdated
Comment thread packages/language-server/src/mcp/tools/query-by-tags.ts
@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch from 38f31d5 to c4f5ecd Compare February 19, 2026 14:26
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch 2 times, most recently from 8e99233 to f8162f4 Compare February 19, 2026 14:42
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

1 similar comment
@ckeller42

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (9)
packages/language-server/src/mcp/tools/query-by-tag-pattern.ts (1)

94-104: Consider adding a default branch for exhaustiveness safety.

The switch on args.matchMode covers all current enum values, but a default: never guard would catch future enum additions at compile time.

Suggested change
     switch (args.matchMode) {
       case 'prefix':
         return tagLower.startsWith(patternLower)
       case 'contains':
         return tagLower.includes(patternLower)
       case 'suffix':
         return tagLower.endsWith(patternLower)
+      default: {
+        const _exhaustive: never = args.matchMode
+        return _exhaustive
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-by-tag-pattern.ts` around lines
94 - 104, The switch in matchesTag does not have a default branch for
exhaustiveness on args.matchMode; add a default case that performs a never-typed
guard (e.g., assign args.matchMode to a never and throw or call an assertNever
helper) so the compiler errors if a new matchMode value is added in the future,
ensuring matchesTag (and the switch) is exhaustive and fails-fast on unhandled
modes.
packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts (1)

12-30: Redundant buildLikeC4Model() calls — other spec files omit them.

Every test here calls await buildLikeC4Model() after validate(), but the handler internally calls languageServices.computedModel(projectId) which builds the model on demand. Sibling test files (e.g., query-graph.spec.ts, query-by-tags.spec.ts) skip this extra step. The calls are harmless but inconsistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts` around
lines 12 - 30, The test calls await buildLikeC4Model() redundantly after await
validate(); remove the extra buildLikeC4Model() invocation(s) in this spec so
the test relies on the handler's lazy model build via
languageServices.computedModel(projectId) (keep validate() and existing uses of
createTestServices(), validate, and services intact).
packages/language-server/src/mcp/tools/query-graph.ts (1)

123-199: Consider extracting the repeated for-of + truncation pattern.

Five of the seven cases share the identical structure: iterate, check cap, push serialized element. A small helper would reduce ~50 lines of duplication.

Example helper
+function collectResults(
+  iterable: Iterable<Parameters<typeof serializeElement>[0]>,
+  results: ElementSummary[],
+  max: number,
+): boolean {
+  for (const item of iterable) {
+    if (results.length >= max) return true
+    results.push(serializeElement(item))
+  }
+  return false
+}

Then each case becomes:

case 'ancestors': {
  truncated = collectResults(element.ancestors(), results, MAX_RESULTS)
  break
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/query-graph.ts` around lines 123 -
199, Extract the repeated iteration + truncation logic into a helper (e.g.,
collectResults(iterable, results, limit, serializer)) that walks an iterable,
calls serializer on each item (use serializeElement), pushes into results, stops
when results.length >= limit, and returns a boolean indicating truncation; then
replace the duplicated blocks in the switch (cases 'ancestors', 'descendants',
'siblings', 'children', 'incomers', 'outgoers') with calls like truncated =
collectResults(element.ancestors(), results, MAX_RESULTS, serializeElement) and
for the filtered calls use the computed filter (const filter =
args.includeIndirect ? 'all' : 'direct') passed as element.incomers(filter) /
element.outgoers(filter); keep the 'parent' case unchanged.
packages/language-server/src/mcp/tools/_common.ts (1)

203-209: Truthy-check on rel.title / rel.technology silently drops empty strings.

Lines 206-207 use truthy checks (if (rel.title), if (rel.technology)) to conditionally include relationship labels and technology. If these fields can legitimately be empty strings (explicitly set but empty), they will be omitted from the neighbor data. This is likely fine for display-oriented labels, but flagging for awareness given the earlier truthy-check discussions on this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/_common.ts` around lines 203 - 209,
The mapping that builds neighbors in the neighbors constant uses truthy checks
(if (rel.title), if (rel.technology)) which will drop legitimate empty string
values; update the conditional checks in that mapping (inside the neighbors
creation in _common.ts where GraphNodeNeighbor is constructed) to test for
undefined/null explicitly (e.g., rel.title !== undefined && rel.title !== null)
so empty strings are preserved when assigning relationshipLabel and technology
on the GraphNodeNeighbor object; keep the logic that still omits undefined/null
values.
packages/language-server/src/mcp/tools/element-diff.ts (1)

196-210: JSON.stringify comparison is order-sensitive for arrays.

Line 204: if metadata values are arrays (as the schema allows string | string[]), two arrays with the same elements in a different order will be reported as "different". This is probably fine for a strict diff, but worth noting if order-insensitive equality is expected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/element-diff.ts` around lines 196 -
210, The comparison in the loop over allKeys (using meta1/meta2 and v1/v2) uses
JSON.stringify to decide equality which treats arrays as order-sensitive; change
the equality check to handle string|string[] per schema by detecting when both
v1 and v2 are arrays and comparing them order-insensitively (e.g., compare
sorted copies or compare as sets) and otherwise fall back to deep equality for
non-array values; update the branch inside the for-loop (where
JSON.stringify(v1) === JSON.stringify(v2) is used) to perform an array-aware
comparison so arrays with the same elements in different order are treated as
equal.
packages/language-server/src/mcp/tools/batch-read-elements.ts (1)

101-101: Redundant runtime guard — Zod input validation already enforces the limit.

The inputSchema at Line 93 applies .max(MAX_IDS) on ids, so the MCP framework rejects over-limit requests before the handler is invoked. The invariant on Line 101 can never fire, adding noise without any safety benefit.

♻️ Proposed removal
-  invariant(args.ids.length <= MAX_IDS, `Maximum ${MAX_IDS} element ids per call`)
-
   const projectId = languageServices.projectsManager.ensureProjectId(args.project)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/batch-read-elements.ts` at line 101,
Remove the redundant runtime guard: delete the invariant(args.ids.length <=
MAX_IDS, `Maximum ${MAX_IDS} element ids per call`) check in
batch-read-elements.ts because inputSchema already enforces .max(MAX_IDS) on
ids; keep the inputSchema validation intact and, after removing the invariant,
also remove the now-unused invariant import (or any related dead code) and
ensure references to MAX_IDS remain for documentation/limits.
packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts (1)

362-366: Pervasive any usage violates the explicit-types guideline; extract the repeated project constant.

Two recurring patterns throughout the file:

  1. {} as any is passed as the handler's extra/context parameter in every single call (~15 sites). If the second parameter has a typed interface, use it; otherwise a minimal as unknown as ExtraType communicates intent better than a bare any cast.
  2. Every structuredContent field access is cast with as any or typed as Array<any>. If the tool functions export their result types (or a StructuredContent type alias exists), those should be used. At minimum, a local interface for each expected payload shape keeps the test self-documenting.
  3. 'default' as ProjectId is repeated verbatim in every handler invocation — extract to a file-scoped constant.
♻️ Illustrative refactor (excerpt)
+const DEFAULT_PROJECT = 'default' as ProjectId
+
+interface SubgraphSummaryResult {
+  root: { id: string; kind: string; title: string; childCount: number }
+  descendants: DescendantSummary[]
+  totalDescendants: number
+  truncated: boolean
+  truncatedByDepth: boolean
+}
 
   it('should summarize cloud system descendants', async () => {
     const [_name, _config, handler] = subgraphSummary(services.likec4.LanguageServices)
     const result = await handler(
-      { elementId: 'cloud', maxDepth: 10, metadataKeys: undefined, project: 'default' as ProjectId },
-      {} as any,
+      { elementId: 'cloud', maxDepth: 10, metadataKeys: undefined, project: DEFAULT_PROJECT },
+      {} as McpExtra,  // or whatever the actual type is
     )
-    const data = result.structuredContent!
-    const root = data['root'] as any
+    const data = result.structuredContent! as SubgraphSummaryResult
+    const root = data.root

As per coding guidelines: "Use TypeScript with explicit types in all TypeScript files; avoid using any".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts`
around lines 362 - 366, Replace pervasive `any` and repeated literal by giving
the handler second-argument and structuredContent explicit types and extracting
the project id constant: define a file-scoped constant like const
DEFAULT_PROJECT: ProjectId = 'default' and use it in all handler(...) calls
instead of 'default' as ProjectId; replace all `{ } as any` passed as the
handler extra/context parameter with a typed value (either import the
appropriate Extra type for the handler or use `{} as unknown as ExtraType`)
referenced where you call subgraphSummary(...) → handler(...); for
structuredContent accesses, import or declare the expected
StructuredContent/ToolResult interfaces (or local minimal interfaces) and use
those types instead of `any`/`Array<any>` so assertions in the spec use concrete
types (refer to symbols subgraphSummary, handler, ProjectId, and
structuredContent in your changes).
packages/language-server/src/mcp/tools/subgraph-summary.ts (1)

172-173: Consider avoiding spread-to-length for relationship counts.

[...element.incoming()].length and [...element.outgoing()].length materialize the full relationship iterables on every BFS node. For elements with many relationships (or when the model's relationship iterables are lazily evaluated), this allocates a transient array per element × up to 200 entries. A loop counter avoids the allocation.

♻️ Proposed refactor
-      incomingCount: [...element.incoming()].length,
-      outgoingCount: [...element.outgoing()].length,
+      incomingCount: (() => { let n = 0; for (const _ of element.incoming()) n++; return n })(),
+      outgoingCount: (() => { let n = 0; for (const _ of element.outgoing()) n++; return n })(),

Or, if the element model exposes a dedicated count API, prefer that over spreading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/subgraph-summary.ts` around lines 172
- 173, Replace the spread-to-length pattern used to compute relationship counts
to avoid allocating transient arrays: instead of incomingCount:
[...element.incoming()].length and outgoingCount:
[...element.outgoing()].length, iterate the iterables and increment counters (or
call any available count API on the element) to compute incomingCount and
outgoingCount; update the code where these properties are set (the
incomingCount/outgoingCount assignments that consume element.incoming() and
element.outgoing()) to use a simple loop-based counter so you don't materialize
the full iterable for each element.
packages/language-server/src/mcp/tools/subgraph-summary.spec.ts (1)

33-34: Pervasive as any casts violate the project's TypeScript guidelines.

Every handler invocation passes {} as any for the extra argument, and every structured-content access uses as any or as Array<any>. Per coding guidelines, all TypeScript files (including specs) should avoid any. Since subgraphSummary's output schema is statically defined, the inferred Zod type or a locally declared interface can replace these casts.

♻️ Example: typed assertion helper
+import type { z } from 'zod/v3'
+import type { descendantSummarySchema } from './subgraph-summary' // export the schema, or define a shared type

+type SubgraphResult = {
+  root: { id: string; kind: string; title: string; childCount: number }
+  descendants: Array<{ id: string; name: string; kind: string; title: string; depth: number; tags: string[]; metadata: Record<string, string | string[]>; childCount: number; incomingCount: number; outgoingCount: number }>
+  totalDescendants: number
+  truncated: boolean
+  truncatedByDepth: boolean
+}

 expect(result.structuredContent).toBeDefined()
-const data = result.structuredContent!
-const root = data['root'] as any
+const data = result.structuredContent as unknown as SubgraphResult
+const root = data.root

As per coding guidelines: "Use TypeScript with explicit types in all TypeScript files; avoid using any".

Also applies to: 85-86, 128-129, 163-164, 195-196, 224-225, 247-249, 282-283

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/mcp/tools/subgraph-summary.spec.ts` around lines
33 - 34, Tests use pervasive `as any` casts when invoking the subgraphSummary
handler and when accessing structured content; replace them with the correct
typed shape by importing or inferring the static output type of the
subgraphSummary schema (or declare a local interface matching its Zod output),
then change invocations that pass "{} as any" to pass an object typed to that
interface and replace "as any" / "as Array<any>" structured-content accesses
with the proper typed property accessors; alternatively add a small typed
assertion helper (e.g., assertTyped<T>(x): T) and use it with the
subgraphSummary output type to remove all `any` usages in the spec and keep
tests type-safe (apply changes to the handler calls and structured-content
accesses referencing subgraphSummary).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/language-server/src/mcp/tools/batch-read-elements.ts`:
- Around line 123-124: Replace the O(n) iterator spreads used to count
relationships by reading the memoized ReadonlySet sizes: use
element.allIncoming.size for incomingCount and element.allOutgoing.size for
outgoingCount instead of [...element.incoming()].length and
[...element.outgoing()].length; update the assignment sites that set
incomingCount/outgoingCount (where element.incoming()/element.outgoing() are
used) to reference element.allIncoming and element.allOutgoing to avoid
unnecessary allocations.

In `@packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts`:
- Line 553: Replace the brittle exact-count assertion on results with a
non-strict lower bound: update the assertion that currently reads
expect(results.length).toBe(2) to
expect(results.length).toBeGreaterThanOrEqual(2) so the test only requires at
least two matched items (the two toContain checks already verify the specific
elements); locate the assertion by the variable name results in the failing test
and change the matcher accordingly.
- Around line 1-3: Remove or replace the incorrect NVIDIA corporate header at
the top of this file: specifically update or delete the SPDX-License-Identifier
and the "Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES" block so the file
uses the project’s standard header/boilerplate (or no header) consistent with
likec4/likec4 contribution policy; ensure the top-of-file comment matches the
repository's expected license header before merging.

---

Duplicate comments:
In `@packages/language-server/src/mcp/tools/find-relationship-paths.spec.ts`:
- Around line 1-3: Remove the proprietary NVIDIA copyright header block at the
top of the file (the SPDX-License-Identifier line followed by the "Copyright (c)
2026 NVIDIA CORPORATION & AFFILIATES" lines) and replace it with the
repository-approved open-source header (e.g., keep a single
SPDX-License-Identifier: MIT or the project's standard license header) so the
file contains only the correct OSS licensing notice; apply the same change to
sibling spec files flagged in the review to ensure consistency.

In `@packages/language-server/src/mcp/tools/find-relationship-paths.ts`:
- Around line 192-231: The PathStep construction uses rel.description.text which
can be null/undefined or the wrong field; update both PathStep creation sites
(the two blocks building relationship.description) to safely obtain a string by
using the correct accessor/utility (e.g., call
flattenMarkdownOrString(rel.description) or otherwise check for null and use
rel.description.txt || rel.description.md) and fall back to an empty string when
absent; ensure you import flattenMarkdownOrString from `@likec4/core` (or apply
the same normalization used elsewhere in the repo) and replace
rel.description.text with the normalized result in both places.

In `@packages/language-server/src/mcp/tools/query-by-tag-pattern.spec.ts`:
- Around line 1-3: The file
packages/language-server/src/mcp/tools/query-by-tag-pattern.spec.ts contains a
proprietary NVIDIA copyright header that doesn't belong in this third-party OSS
repo; remove that header block (the SPDX and NVIDIA copyright lines at the top
of query-by-tag-pattern.spec.ts) and replace it with the repository's standard
OSS license header or no header as per project policy so the file matches other
sibling spec files.

In `@packages/language-server/src/mcp/tools/query-by-tags.spec.ts`:
- Around line 1-3: The file contains a proprietary NVIDIA copyright header
string "Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights
reserved." which is inappropriate for this third‑party OSS repo; confirm with
the maintainers whether that IP attribution is intentional, and if not, remove
or replace that header in
packages/language-server/src/mcp/tools/query-by-tags.spec.ts (and the sibling
spec files) with the project's approved OSS header or a neutral contributor
header per repository guidelines before merging.

In `@packages/language-server/src/mcp/tools/query-graph.spec.ts`:
- Around line 1-3: The file-level header contains NVIDIA-specific copyright text
that does not belong in this third‑party OSS repo; replace the proprietary
header block with the appropriate open-source header (e.g., keep or add only the
SPDX identifier "SPDX-License-Identifier: MIT" or the project's standard OSS
header used by sibling spec files) and remove the "NVIDIA CORPORATION &
AFFILIATES" copyright line so the top-of-file comment matches other spec files.

In `@packages/language-server/src/mcp/tools/query-graph.ts`:
- Around line 1-3: The file currently contains a proprietary NVIDIA copyright
header ("Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.") which must be
removed and replaced with the repository's approved OSS header; update the
top-of-file header that includes "SPDX-License-Identifier" and remove or replace
the NVIDIA copyright line with the project's standard copyright/attribution text
(or only the SPDX line if that is the repo convention) so the header matches the
third-party OSS repository policy.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts`:
- Around line 1-3: Remove the NVIDIA-specific copyright header text from the top
of this third‑party test file and replace it with the repository's approved OSS
header (e.g., keep the SPDX-License-Identifier: MIT line but remove "Copyright
(c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved."), or otherwise
substitute the repository/project owner text used across other files; update the
header in the same file (query-outgoers-graph.spec.ts) so it matches the
project's standard license header.

In `@packages/language-server/src/mcp/tools/read-project-summary.ts`:
- Around line 165-166: project.documents may be absent at runtime because the
fallback model in LikeC4Model doesn't include a documents property, so retain
the defensive access; change the expression to use a safe default like
(project.documents ?? []).map(...) or keep project.documents?.map(...) but
ensure the result is coerced to an array (e.g., sources:
project.documents?.map(d => d.fsPath) ?? []) so the code in read-project-summary
(the sources assignment using project.documents?.map) and calls from
languageServices.project(projectId) won't throw when documents is missing.

In `@packages/language-server/src/mcp/tools/subgraph-summary.spec.ts`:
- Around line 252-257: The assertion uses the boolean expression ('text' in
content && content.text) which can yield false for non-text ContentBlock and
cause a confusing Vitest type error; change it to a ternary that yields a string
(e.g. ('text' in content ? content.text : '')) so the expect receives a string
and .toContain('not found') behaves correctly—update the test around the local
variables result and content in subgraph-summary.spec.ts to use the ternary
guard instead of the && expression.

In `@packages/mcp/README.md`:
- Around line 62-63: The descriptions for query-incomers-graph and
query-outgoers-graph are inverted; update README entries so that
query-incomers-graph describes the complete graph of downstream
dependents/callers (nodes Y where Y → X) and query-outgoers-graph describes the
complete graph of upstream producers/dependencies (nodes Y where X → Y); locate
the two bullets referencing query-incomers-graph and query-outgoers-graph and
swap or rewrite their wording to match the standard graph semantics (incomers =
callers/downstream, outgoers = dependencies/upstream).

---

Nitpick comments:
In `@packages/language-server/src/mcp/tools/_common.ts`:
- Around line 203-209: The mapping that builds neighbors in the neighbors
constant uses truthy checks (if (rel.title), if (rel.technology)) which will
drop legitimate empty string values; update the conditional checks in that
mapping (inside the neighbors creation in _common.ts where GraphNodeNeighbor is
constructed) to test for undefined/null explicitly (e.g., rel.title !==
undefined && rel.title !== null) so empty strings are preserved when assigning
relationshipLabel and technology on the GraphNodeNeighbor object; keep the logic
that still omits undefined/null values.

In `@packages/language-server/src/mcp/tools/batch-read-elements.ts`:
- Line 101: Remove the redundant runtime guard: delete the
invariant(args.ids.length <= MAX_IDS, `Maximum ${MAX_IDS} element ids per call`)
check in batch-read-elements.ts because inputSchema already enforces
.max(MAX_IDS) on ids; keep the inputSchema validation intact and, after removing
the invariant, also remove the now-unused invariant import (or any related dead
code) and ensure references to MAX_IDS remain for documentation/limits.

In `@packages/language-server/src/mcp/tools/cloud-system-integration.spec.ts`:
- Around line 362-366: Replace pervasive `any` and repeated literal by giving
the handler second-argument and structuredContent explicit types and extracting
the project id constant: define a file-scoped constant like const
DEFAULT_PROJECT: ProjectId = 'default' and use it in all handler(...) calls
instead of 'default' as ProjectId; replace all `{ } as any` passed as the
handler extra/context parameter with a typed value (either import the
appropriate Extra type for the handler or use `{} as unknown as ExtraType`)
referenced where you call subgraphSummary(...) → handler(...); for
structuredContent accesses, import or declare the expected
StructuredContent/ToolResult interfaces (or local minimal interfaces) and use
those types instead of `any`/`Array<any>` so assertions in the spec use concrete
types (refer to symbols subgraphSummary, handler, ProjectId, and
structuredContent in your changes).

In `@packages/language-server/src/mcp/tools/element-diff.ts`:
- Around line 196-210: The comparison in the loop over allKeys (using
meta1/meta2 and v1/v2) uses JSON.stringify to decide equality which treats
arrays as order-sensitive; change the equality check to handle string|string[]
per schema by detecting when both v1 and v2 are arrays and comparing them
order-insensitively (e.g., compare sorted copies or compare as sets) and
otherwise fall back to deep equality for non-array values; update the branch
inside the for-loop (where JSON.stringify(v1) === JSON.stringify(v2) is used) to
perform an array-aware comparison so arrays with the same elements in different
order are treated as equal.

In `@packages/language-server/src/mcp/tools/query-by-tag-pattern.ts`:
- Around line 94-104: The switch in matchesTag does not have a default branch
for exhaustiveness on args.matchMode; add a default case that performs a
never-typed guard (e.g., assign args.matchMode to a never and throw or call an
assertNever helper) so the compiler errors if a new matchMode value is added in
the future, ensuring matchesTag (and the switch) is exhaustive and fails-fast on
unhandled modes.

In `@packages/language-server/src/mcp/tools/query-graph.ts`:
- Around line 123-199: Extract the repeated iteration + truncation logic into a
helper (e.g., collectResults(iterable, results, limit, serializer)) that walks
an iterable, calls serializer on each item (use serializeElement), pushes into
results, stops when results.length >= limit, and returns a boolean indicating
truncation; then replace the duplicated blocks in the switch (cases 'ancestors',
'descendants', 'siblings', 'children', 'incomers', 'outgoers') with calls like
truncated = collectResults(element.ancestors(), results, MAX_RESULTS,
serializeElement) and for the filtered calls use the computed filter (const
filter = args.includeIndirect ? 'all' : 'direct') passed as
element.incomers(filter) / element.outgoers(filter); keep the 'parent' case
unchanged.

In `@packages/language-server/src/mcp/tools/query-outgoers-graph.spec.ts`:
- Around line 12-30: The test calls await buildLikeC4Model() redundantly after
await validate(); remove the extra buildLikeC4Model() invocation(s) in this spec
so the test relies on the handler's lazy model build via
languageServices.computedModel(projectId) (keep validate() and existing uses of
createTestServices(), validate, and services intact).

In `@packages/language-server/src/mcp/tools/subgraph-summary.spec.ts`:
- Around line 33-34: Tests use pervasive `as any` casts when invoking the
subgraphSummary handler and when accessing structured content; replace them with
the correct typed shape by importing or inferring the static output type of the
subgraphSummary schema (or declare a local interface matching its Zod output),
then change invocations that pass "{} as any" to pass an object typed to that
interface and replace "as any" / "as Array<any>" structured-content accesses
with the proper typed property accessors; alternatively add a small typed
assertion helper (e.g., assertTyped<T>(x): T) and use it with the
subgraphSummary output type to remove all `any` usages in the spec and keep
tests type-safe (apply changes to the handler calls and structured-content
accesses referencing subgraphSummary).

In `@packages/language-server/src/mcp/tools/subgraph-summary.ts`:
- Around line 172-173: Replace the spread-to-length pattern used to compute
relationship counts to avoid allocating transient arrays: instead of
incomingCount: [...element.incoming()].length and outgoingCount:
[...element.outgoing()].length, iterate the iterables and increment counters (or
call any available count API on the element) to compute incomingCount and
outgoingCount; update the code where these properties are set (the
incomingCount/outgoingCount assignments that consume element.incoming() and
element.outgoing()) to use a simple loop-based counter so you don't materialize
the full iterable for each element.

Comment thread packages/language-server/src/mcp/tools/batch-read-elements.ts Outdated
Add new MCP tools for advanced graph traversal and filtering:
- query-graph: Navigate element hierarchy (ancestors, descendants, siblings, children, parent, incomers, outgoers)
- query-incomers-graph: Find elements that depend on a target element
- query-outgoers-graph: Find elements a source element depends on
- query-by-metadata: Search elements by metadata with exact/contains/exists matching
- query-by-tags: Filter elements by tags with boolean logic (allOf, anyOf, noneOf)
- find-relationship-paths: Discover multi-hop relationship paths between elements using BFS

Enhanced read-project-summary to include serialized project configuration.
Extended project config schema to support custom metadata field.
@ckeller42 ckeller42 force-pushed the feat/improve-mcp-data branch from f8162f4 to 0587b66 Compare February 19, 2026 16:51
@ckeller42 ckeller42 marked this pull request as ready for review February 19, 2026 21:11

@davydkov davydkov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for enhancing MCP!

@davydkov davydkov merged commit 45145ec into likec4:main Feb 20, 2026
13 checks passed
@likec4-ci likec4-ci Bot mentioned this pull request Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants