Skip to content

Mcp#385

Open
whamm86 wants to merge 22 commits into
mainfrom
mcp
Open

Mcp#385
whamm86 wants to merge 22 commits into
mainfrom
mcp

Conversation

@whamm86

@whamm86 whamm86 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Add possibility to make queries with MCP server support and small refactoring for data access in CrudOperator

aorzelskiGH and others added 21 commits June 19, 2026 08:23
Expose the AASPE query pipeline via a Model Context Protocol (MCP) server
so LLM clients can search and read AAS data without writing raw AASQL.

Tools (src/IO.Swagger.Lib.V3/MCP/McpQueryTools.cs):
- aas_query / aas_count: slot-based search over shells/submodels; builds
  AASQL JSON ($and/$or) and returns identifiers. idShortPath without a dot
  resolves by idShort; numeric eq/ne when the value parses as a number.
- aas_get_submodel / aas_get_element: read a submodel or a single SME in
  compact "value" or full AAS-JSON format.
- aas_get_shell: read the AAS (assetInformation + submodel references).

Registered as Streamable HTTP transport at /api/v3.0/mcp. Compact per-call
console logging via [MCP] lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the initial MCP server with higher-level tools and a reduced
endpoint for weak models, plus query-semantics and robustness fixes.

New tools (McpQueryTools.cs):
- aas_get_shell: read the AAS (assetInformation + submodel references)
- aas_get_product: AAS + values of ALL its submodels in one call;
  accepts an AAS id or a submodel id (resolves the shell automatically)
- aas_find_product: search + full product in a single call, so models
  that do not chain tool calls still get everything at once

Two endpoints (ServerConfiguration.cs): /mcp exposes all tools; the new
/mcp-basic exposes only aas_find_product (for weak models that otherwise
pick the wrong tool or stop after the first call). Implemented via MCP
request filters that inspect the request path.

Query/serialization refinements:
- format=value|full on the get_*/find tools (value-only or full AAS-JSON
  incl. semanticId/valueType/qualifiers)
- numeric eq/ne when the value parses as a number (NumberStyles.Float so
  "1,32" stays a string); idShortPath without a dot resolves by idShort;
  empty sme field defaults to "value"
- validation errors returned as readable {error} results instead of
  opaque failures; NotFoundException handled as {found:false}
- nextStep hints on aas_query/aas_count; compact [MCP] console logging

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Small models (e.g. Qwen2.5:7b) call the tool but fill the conditions[]
schema incorrectly (path in field, invented operators/fields). Give them
a tool with flat parameters and route models to an endpoint matching
their capability.

- aas_find_product_simple(idShort, value, format): finds a product by a
  single property (idShort = value) and returns the whole product, with
  no scope/op/conditions to fill wrong. Shares FindProductCore with
  aas_find_product.
- Three endpoints via the path-based request filter:
    /mcp         all tools (strong models that chain)
    /mcp-basic   aas_find_product (conditions; mid models, complex search)
    /mcp-simple  aas_find_product_simple only (weak models)
  Tool visibility and call rejection are driven by the request path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Small models (7-8B) confabulate when given the full product (many
submodels with bulky CAD/BoM/documentation), inventing fields that are
not in the data. Make the simple tool's output minimal and focused.

- Drop the format parameter: aas_find_product_simple always returns the
  compact value form (full overwhelms small models).
- coreOnly filter: BuildProductObject/FindProductCore gain a coreOnly
  flag; the simple tool sets it to skip noisy file/doc submodels
  (CAD, BoM_SpareParts, HandoverDocumentation) by idShort keyword,
  returning only Nameplate / TechnicalData / CarbonFootprint. Smaller
  payload also helps fit small context windows.

The full toolset (aas_find_product, aas_get_product) is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Driven by a real at-scale scenario (filtering power supplies and building
a table) that needed ~20 query+get round-trips. Combine search with
projection and add list/batch operations.

- aas_query select=[idShortPaths]: return a row of those field values per
  hit (projection/table) instead of only identifiers; lang (default "en")
  collapses MultiLanguageProperty cells to one language.
- in operator: condition.Values + op="in" expands to an OR of eq
  comparisons (reuses the full field/path/numeric logic).
- aas_get_submodels(identifiers[], format, select?, lang?): fetch many
  submodels in one call; with select it returns a table.
- aas_get_shells(identifiers[], format): fetch many shells in one call.
- Per-call timing: each tool logs "[MCP] <tool> done in X ms" in addition
  to the entry line, for performance analysis on large databases.

Shared helpers BuildProjectionRow and BuildShellObject avoid duplication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
eq/ne built a purely numeric comparison when the value parsed as a number,
so eq "2908936" (a string-typed ManufacturerArticleNumber that happens to
be all digits) compared against the numeric column and never matched —
which also broke the new in-operator for article-number lists.

eq now emits an OR of string and numeric comparison (ne emits an AND of
both negations) when the value is numeric, since the valueType
(xs:string vs xs:double) is unknown at query time. So eq "80" still finds
a Double 80 and eq "2908936" finds the digit-only string id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Projecting a leaf idShort like "Product_type" returned the value from an
accessory subtree (Part_relation...Associated_part.Manufacturer.Product_type,
e.g. a mounting adapter "UWA 130") instead of the actual product, because
the leaf search returned the first match anywhere in the tree.

GetElementByPath now prefers matches outside accessory branches
(Part_relation / Associated_part), falling back to the first match only if
the field exists exclusively under accessories.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An aas.* condition in a submodel query produces invalid SQL
("no such column: aas.IdShort"), which the engine raised as a
SqliteException that the tools did not catch -> unhandled exception and an
opaque error. Wrap QueryGetSMs in TryQuery so engine failures become a
readable {error} the model can recover from, instead of crashing the call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A bare leaf idShort with a value comparison (e.g. Power_output gt 500)
expanded to $and[$sme#idShort==X, $sme#value op Y], which is NOT correlated
to the same element: it matched submodels that have ANY element named X AND
ANY element with a value op Y -> false positives (123 instead of 16; devices
with Power_output=240 but some other value>500 matched).

Use the engine's correlated path mechanism instead: $or of the top-level
path ($sme.<leaf>#field) and the nested wildcard path ($sme.%.<leaf>#field,
% = any path prefix). Each is a single correlated path subquery (idShortPath
and value in the same row).

Verified: flowMax eq 350 now returns 0 (was a false positive because
presMax=350 exists), flowMax eq 80 -> 1, top-level SerialNumber -> 1,
numeric gt still works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aas_count previously ran the full QueryGetSMs path, which materializes every
matching submodel (the slow "Collect results" phase) just to count them, and
was capped at 500. On larger data this took 5-13 s and gave wrong totals
above the cap.

Add a dedicated count-only path that stops after the id query and returns
result.Count, skipping collection entirely:
- Query.GetQueryDataCount: mirrors GetQueryData setup, returns ids.Count.
- EntityFrameworkPersistenceService: implement the DbRequestOp.QueryCountSMs
  case (was commented out) -> GetQueryDataCount.
- IDbRequestHandlerService/DbRequestHandlerService.QueryCountSMs: add
  ResultType param (set ResultType.Submodel) so it counts submodels; updated
  the GraphQL CountSMs caller accordingly.
- McpQueryTools.AasCount: call QueryCountSMs with pageSize=int.MaxValue
  (LIMIT covers all rows on SQLite & Postgres); drop the 500 cap and the
  now-obsolete "capped" field; wrap in try/catch for readable errors.

Verified locally: aas_count == aas_query counts (7,6,1), engine logs
"Count query in 4-6 ms" vs the prior multi-second collect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	src/AasxServerDB/Query.cs
The OR-distribution fallback (no security merged) did not strip a redundant enclosing paren, so a fully-wrapped top-level OR stayed one monolithic branch (full SMSets scan) instead of splitting into value-/index-driven branches. Apply StripEnclosingParens before SplitTopLevelOr in both the joined and direct union/temp builders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A non-selective contains (e.g. 'Phoenix' on a Phoenix-Contact dataset) made the value-driven IN/FTS path materialize a huge match set (~124s). A capped FTS probe (LIMIT N over ValueSets_fts) now classifies each single leading-wildcard $sme#value contains: 'common' (>= N matches) -> correlated EXISTS (early-stop under ORDER BY/LIMIT); rare/zero -> unchanged value-driven IN/FTS (already fast). The matched pattern is passed to ApplySqliteTrigramIndex as a skip-set so the FTS candidate filter is not re-injected into the EXISTS. Cap is settable for tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add per-tool title + read-only annotations via [McpServerTool] attributes, and inject _meta in the ListTools filter: securitySchemes [{type:noauth}] (the _meta location ChatGPT reads; typed SDK cannot emit a custom top-level field) plus openai/toolInvocation/invoking|invoked status text. name/description/inputSchema and behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A bare $sme#value comparison now becomes its own existential over the
submodel's elements (BuildOverallSqlNode routes it through the value-match
builder) instead of a ValueSets join. Multiple bare value conditions under
$and are emitted as separate EXISTS(A) AND EXISTS(B) (per-element semantics:
different elements may satisfy each), fixing the previous wrong 0-result
EXISTS(A AND B) on the same row.

Extend the common-value probe (previously contains-only) to exact equality
and prefix (starts-with): a capped index probe classifies a frequent value
and routes it to the correlated EXISTS (early-stop under ORDER BY/LIMIT)
instead of materializing the full value-driven IN set. Fixes a regression
where a common '=' value with LIMIT 3 materialized its entire match set
(~23 s -> ~0.4 s); a common prefix likewise (-> ~6 ms).

Tests: bare-value FILTER-inside-value-match, common/rare equality, common
prefix. 50/50 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ aorzelskiGH
❌ whamm86
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

4 participants