Remove MCP
This commit is contained in:
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"agenticcode": {
|
|
||||||
"type": "sse",
|
|
||||||
"url": "http://localhost:8787/mcp/sse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
CLAUDE.md
31
CLAUDE.md
@@ -24,10 +24,9 @@
|
|||||||
* **A deep refresh is long and mutates the graph.** Do not abort one halfway: the earlier enrichment steps
|
* **A deep refresh is long and mutates the graph.** Do not abort one halfway: the earlier enrichment steps
|
||||||
are already committed, so the graph is left partially updated and every later query silently answers from
|
are already committed, so the graph is left partially updated and every later query silently answers from
|
||||||
it. If one must be stopped, say plainly that the graph is now in a half-updated state.
|
it. If one must be stopped, say plainly that the graph is now in a half-updated state.
|
||||||
* **Keep REST API, MCP, and `ac-cli` in sync.** Every REST endpoint added or changed in
|
* **Keep the REST API and `ac-cli` in sync.** Every REST endpoint added or changed in
|
||||||
`ac-code-server/.../api/*Resource.java` MUST have a matching MCP tool in `.../mcp/Mcp*Tools.java` **and** a matching
|
`ac-code-server/.../api/*Resource.java` MUST have a matching `ac-cli` command, delivered in the same change. Treat
|
||||||
`ac-cli` command, delivered in the same change. Treat REST + MCP + CLI as one unit of work — none of the three is
|
REST + CLI as one unit of work — neither is "done" without the other.
|
||||||
"done" without the other two.
|
|
||||||
## 1. Core Rules
|
## 1. Core Rules
|
||||||
|
|
||||||
- Never assume. Always read files first with tools.
|
- Never assume. Always read files first with tools.
|
||||||
@@ -57,31 +56,29 @@
|
|||||||
output. Usage guide: `x-docs/mcp-api-usage-ac-implementation.md`. Re-ingest after code changes
|
output. Usage guide: `x-docs/mcp-api-usage-ac-implementation.md`. Re-ingest after code changes
|
||||||
(`ac refresh` / `POST /api/projects/ac/refresh`, or `--deep`/`?deep=true` for a full field-level
|
(`ac refresh` / `POST /api/projects/ac/refresh`, or `--deep`/`?deep=true` for a full field-level
|
||||||
pass) before trusting query results.
|
pass) before trusting query results.
|
||||||
- **Tool priority: MCP first, then REST API, then the `ac` CLI, then grep/Explore.** Try the
|
- **Tool priority: REST API first, then the `ac` CLI, then grep/Explore.** Query the REST endpoints
|
||||||
`mcp__agenticcode__*` MCP tools before anything else. If an MCP call fails or misbehaves (e.g. a
|
(`GET /api/projects/ac/modules/{name}/...`) before anything else. The `ac <command>` CLI (`ac callers`,
|
||||||
session/protocol error), fall back to the equivalent REST endpoint
|
`ac callees`,
|
||||||
(`GET /api/projects/ac/modules/{name}/...`) rather than abandoning the API — don't let one broken
|
|
||||||
MCP call push you straight to grep. The `ac <command>` CLI (`ac callers`, `ac callees`,
|
|
||||||
`ac call-tree`, `ac context`, `ac db-accesses`, `ac refresh`, ...) is a convenience wrapper over the same REST API,
|
`ac call-tree`, `ac context`, `ac db-accesses`, `ac refresh`, ...) is a convenience wrapper over the same REST API,
|
||||||
useful for quick manual checks. **If the server is unavailable** (`http://localhost:8787`
|
useful for quick manual checks. **If the server is unavailable** (`http://localhost:8787`
|
||||||
unreachable), **ask the user to run `./manage-ac.sh deploy`** — never start it yourself (see Hard Rules).
|
unreachable), **ask the user to run `./manage-ac.sh deploy`** — never start it yourself (see Hard Rules).
|
||||||
**Only when it still can't answer the question** (info the API doesn't expose, or the analysis needs
|
**Only when it still can't answer the question** (info the API doesn't expose, or the analysis needs
|
||||||
exact source text/comments/formatting) fall back to normal code analysis (Read/Grep/Explore agent)
|
exact source text/comments/formatting) fall back to normal code analysis (Read/Grep/Explore agent)
|
||||||
exactly as you would on any other codebase.
|
exactly as you would on any other codebase.
|
||||||
- **If a needed capability is missing from MCP/API/CLI** (not just unreachable, but the question is
|
- **If a needed capability is missing from the API/CLI** (not just unreachable, but the question is
|
||||||
one none of them can answer at all), don't silently fall back to grep and move on — after finishing
|
one none of them can answer at all), don't silently fall back to grep and move on — after finishing
|
||||||
the task with the grep-based fallback, use `AskUserQuestion` to tell the user what was missing and
|
the task with the grep-based fallback, use `AskUserQuestion` to tell the user what was missing and
|
||||||
ask whether it should be added as a feature in `x-docs/roadmap.md`.
|
ask whether it should be added as a feature in `x-docs/roadmap.md`.
|
||||||
|
|
||||||
## 2. Mandatory 4-Phase Workflow
|
## 2. Mandatory 4-Phase Workflow
|
||||||
|
|
||||||
0. **Clarify Intent** -Assess if the user's request is fully clear.
|
``0. **Clarify Intent** -Assess if the user's request is fully clear.
|
||||||
- **Clear**: State your understanding and proceed.
|
- **Clear**: State your understanding and proceed.
|
||||||
- **Unclear**: Use `AskUserQuestion` tool with concrete options. Repeat until unambiguous.
|
- **Unclear**: Use `AskUserQuestion` tool with concrete options. Repeat until unambiguous.
|
||||||
1. **PROPOSAL** – Clear plan + impact. Wait for `OK PROPOSAL`.
|
1. **PROPOSAL** – Clear plan + impact. Wait for `OK PROPOSAL`.
|
||||||
2. **VALIDATE** – Brutal self-critique (NullAway, layering, regressions). Wait for `OK VALIDATE`.
|
2. **VALIDATE** – Brutal self-critique (NullAway, layering, regressions). Wait for `OK VALIDATE`.
|
||||||
3. **IMPLEMENT** – One unit at a time. Show exact diff only.
|
3. **IMPLEMENT** – One unit at a time. Show exact diff only.
|
||||||
4. **VERIFICATION** – Run relevant tests. Suggest a commit message, but do not commit unless asked (see Core Rules).
|
4. **VERIFICATION** – Run relevant tests. Suggest a commit message, but do not commit unless asked (see Core Rules).``
|
||||||
|
|
||||||
Quarkus-based server for parsing, storing, and agentically querying source code (Natural/Software AG and Java).
|
Quarkus-based server for parsing, storing, and agentically querying source code (Natural/Software AG and Java).
|
||||||
Programs are parsed into a unified AST, persisted as a graph in Neo4j, enriched with semantic information, and exposed via an agent-optimized API.
|
Programs are parsed into a unified AST, persisted as a graph in Neo4j, enriched with semantic information, and exposed via an agent-optimized API.
|
||||||
@@ -89,14 +86,14 @@ Programs are parsed into a unified AST, persisted as a graph in Neo4j, enriched
|
|||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
```
|
```
|
||||||
Parser Layer → Unified AST → Neo4j Graph DB → Enrichment → Agentic API (REST + MCP)
|
Parser Layer → Unified AST → Neo4j Graph DB → Enrichment → Agentic API (REST)
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Natural Parser**: custom implementation (no OSS parser available for Software AG Natural)
|
- **Natural Parser**: custom implementation (no OSS parser available for Software AG Natural)
|
||||||
- **Java Parser**: JavaParser library
|
- **Java Parser**: JavaParser library
|
||||||
- **Graph Store**: Neo4j — nodes for modules, functions, variables, data structures, DB accesses
|
- **Graph Store**: Neo4j — nodes for modules, functions, variables, data structures, DB accesses
|
||||||
- **Enrichment**: call graph, identifier index, data structures, ADABAS/SQL access patterns
|
- **Enrichment**: call graph, identifier index, data structures, ADABAS/SQL access patterns
|
||||||
- **API**: Quarkus REST (JAX-RS) + MCP endpoint, optimized for AI agent tool use
|
- **API**: Quarkus REST (JAX-RS), optimized for AI agent tool use
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
@@ -123,8 +120,7 @@ agenticcode/
|
|||||||
│ ├── src/main/java/de/agenticcode/
|
│ ├── src/main/java/de/agenticcode/
|
||||||
│ │ ├── api/ # JAX-RS Resources
|
│ │ ├── api/ # JAX-RS Resources
|
||||||
│ │ ├── service/ # Business logic
|
│ │ ├── service/ # Business logic
|
||||||
│ │ ├── enrichment/ # Enrichment pipelines
|
│ │ └── enrichment/ # Enrichment pipelines
|
||||||
│ │ └── mcp/ # MCP tool endpoints
|
|
||||||
│ └── src/main/resources/
|
│ └── src/main/resources/
|
||||||
│ └── application.properties
|
│ └── application.properties
|
||||||
├── ac-parser-natural/ # Natural parser module
|
├── ac-parser-natural/ # Natural parser module
|
||||||
@@ -236,7 +232,7 @@ The API is designed primarily for AI agent tool use:
|
|||||||
- `GET /api/search/identifier?name=...` — find an identifier across all modules
|
- `GET /api/search/identifier?name=...` — find an identifier across all modules
|
||||||
- `POST /api/ingest` — parse and ingest source code into the graph
|
- `POST /api/ingest` — parse and ingest source code into the graph
|
||||||
|
|
||||||
MCP endpoint structure: `@docs/api-endpoints.md`
|
Endpoint structure: `@docs/api-endpoints.md`
|
||||||
|
|
||||||
## Natural Parser: Key Constructs
|
## Natural Parser: Key Constructs
|
||||||
|
|
||||||
@@ -297,5 +293,4 @@ mvn test -Dtest="*IT"
|
|||||||
| Multi-module Maven | Parsers are independently testable and replaceable |
|
| Multi-module Maven | Parsers are independently testable and replaceable |
|
||||||
| Custom Natural parser | No OSS parser available for Software AG Natural |
|
| Custom Natural parser | No OSS parser available for Software AG Natural |
|
||||||
| JavaParser over tree-sitter | Mature Java library, type-safe AST API |
|
| JavaParser over tree-sitter | Mature Java library, type-safe AST API |
|
||||||
| MCP alongside REST | Native integration with Claude Code and other AI agents |
|
|
||||||
|
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
AgenticCode ingests source code (Natural/Software AG and Java), parses it into a unified AST, persists it in Neo4j,
|
AgenticCode ingests source code (Natural/Software AG and Java), parses it into a unified AST, persists it in Neo4j,
|
||||||
enriches it with semantic information (call graphs, DB accesses, data structures, dynamic-dispatch resolution), and
|
enriches it with semantic information (call graphs, DB accesses, data structures, dynamic-dispatch resolution), and
|
||||||
exposes everything through an agent-ready **REST + MCP** API, a **CLI**, and a **web UI**.
|
exposes everything through an agent-ready **REST** API, a **CLI**, and a **web UI**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -37,12 +37,11 @@ flowchart TD
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph API["Agentic API — Quarkus"]
|
subgraph API["Agentic API — Quarkus"]
|
||||||
REST["REST API\nJAX-RS · RESTEasy Reactive"]
|
REST["REST API\nJAX-RS · RESTEasy Reactive · tool-use optimized"]
|
||||||
MCP["MCP Endpoint\nHTTP/SSE · tool-use optimized"]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Clients["Clients"]
|
subgraph Clients["Clients"]
|
||||||
AG(["Claude / LLM Agent\nvia MCP"])
|
AG(["Claude / LLM Agent\nvia REST"])
|
||||||
CLI(["ac CLI"])
|
CLI(["ac CLI"])
|
||||||
UI(["Web UI\nReact + Vite"])
|
UI(["Web UI\nReact + Vite"])
|
||||||
end
|
end
|
||||||
@@ -56,9 +55,7 @@ flowchart TD
|
|||||||
PI --> NEO
|
PI --> NEO
|
||||||
NEO --> CG & DB & ID & DS
|
NEO --> CG & DB & ID & DS
|
||||||
CG & DB & ID & DS --> REST
|
CG & DB & ID & DS --> REST
|
||||||
CG & DB & ID & DS --> MCP
|
|
||||||
REST --> AG & CLI & UI
|
REST --> AG & CLI & UI
|
||||||
MCP --> AG
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -85,7 +82,6 @@ Once up:
|
|||||||
|
|
||||||
- **Web UI** — `http://localhost:5174`
|
- **Web UI** — `http://localhost:5174`
|
||||||
- **REST API** — `http://localhost:8787/api`
|
- **REST API** — `http://localhost:8787/api`
|
||||||
- **MCP endpoint** — `http://localhost:8787/mcp/sse` (HTTP/SSE transport)
|
|
||||||
- **OpenAPI / health** — `http://localhost:8787/q/openapi`, `http://localhost:8787/q/health`
|
- **OpenAPI / health** — `http://localhost:8787/q/openapi`, `http://localhost:8787/q/health`
|
||||||
- **Neo4j browser** — `http://localhost:7474` (credentials `neo4j` / `agenticcode`)
|
- **Neo4j browser** — `http://localhost:7474` (credentials `neo4j` / `agenticcode`)
|
||||||
|
|
||||||
@@ -241,19 +237,14 @@ stays visible as an unresolved site.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## MCP (Claude Code & other agents)
|
## Agent usage (Claude Code & other agents)
|
||||||
|
|
||||||
The server exposes every query capability as MCP tools over HTTP/SSE at `http://localhost:8787/mcp/sse` (server name
|
The server exposes every query capability as REST endpoints under `http://localhost:8787/api` — e.g.
|
||||||
`agenticcode`). Register it with Claude Code:
|
`/projects`, `/projects/{p}/modules`, `.../callers`, `.../callees`, `.../call-tree`, `.../db-accesses`,
|
||||||
|
`/search/identifier`, `.../context`, `.../payload`, `.../dispatch-table`, `/variables/{n}/flow-forward` and
|
||||||
```bash
|
`/flow-backward`, `/variables/{n}/field-flow`, `/variables/{n}/reads` and `/writes`,
|
||||||
claude mcp add --transport sse agenticcode http://localhost:8787/mcp/sse
|
`/dynamic-calls/unresolved`, `/dynamic-calls/overrides`, `/refresh`, and more. The OpenAPI spec is served at
|
||||||
```
|
`/q/openapi`. A full usage guide with response fields and semantics lives in [
|
||||||
|
|
||||||
Tools mirror the REST surface — e.g. `list_projects`, `list_modules`, `callers`, `callees`, `call_tree`, `db_accesses`,
|
|
||||||
`search_identifier`, `module_context`, `module_payload`, `module_dispatch_table`, `flow_forward`/`flow_backward`,
|
|
||||||
`field_flow`, `variable_reads`/`variable_writes`, `list_unresolved_dynamic_calls`, `set_dynamic_call_override`,
|
|
||||||
`refresh`, and more. A full usage guide with response fields and semantics lives in [
|
|
||||||
`x-docs/mcp-api-usage-ac-implementation.md`](x-docs/mcp-api-usage-ac-implementation.md).
|
`x-docs/mcp-api-usage-ac-implementation.md`](x-docs/mcp-api-usage-ac-implementation.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -416,7 +407,7 @@ own-subroutine `PERFORM` wiring). `search-identifier` takes `--type`, `--module`
|
|||||||
names, a leading sigil (`#`, `&`, `+`) is optional — `ac search-identifier I-LINE-LEV` and `'#I-LINE-LEV'` are
|
names, a leading sigil (`#`, `&`, `+`) is optional — `ac search-identifier I-LINE-LEV` and `'#I-LINE-LEV'` are
|
||||||
equivalent.
|
equivalent.
|
||||||
|
|
||||||
Everything the CLI does is also available as a REST endpoint and an MCP tool — the three are kept in lockstep.
|
Everything the CLI does is also available as a REST endpoint — the two are kept in lockstep.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -428,7 +419,6 @@ Everything the CLI does is also available as a REST endpoint and an MCP tool —
|
|||||||
| Language | Java 21 — Records, Sealed Classes, Virtual Threads |
|
| Language | Java 21 — Records, Sealed Classes, Virtual Threads |
|
||||||
| Graph DB | Neo4j 5.x via `neo4j-java-driver` |
|
| Graph DB | Neo4j 5.x via `neo4j-java-driver` |
|
||||||
| REST | RESTEasy Reactive (JAX-RS) |
|
| REST | RESTEasy Reactive (JAX-RS) |
|
||||||
| MCP | `quarkus-mcp-server-sse` (HTTP/SSE) |
|
|
||||||
| Natural Parser | Custom implementation |
|
| Natural Parser | Custom implementation |
|
||||||
| Java Parser | [JavaParser](https://javaparser.org) |
|
| Java Parser | [JavaParser](https://javaparser.org) |
|
||||||
| CLI | Picocli uber-jar |
|
| CLI | Picocli uber-jar |
|
||||||
@@ -486,7 +476,7 @@ Full schema with Cypher examples: [`x-docs/ast-graph-schema.md`](x-docs/ast-grap
|
|||||||
|
|
||||||
```
|
```
|
||||||
agenticcode/
|
agenticcode/
|
||||||
├── ac-code-server/ # Quarkus application (REST + MCP)
|
├── ac-code-server/ # Quarkus application (REST API)
|
||||||
├── ac-cli/ # Picocli command-line client (`ac`)
|
├── ac-cli/ # Picocli command-line client (`ac`)
|
||||||
├── ac-ui/ # React + Vite web UI
|
├── ac-ui/ # React + Vite web UI
|
||||||
├── ac-parser-core/ # Shared AST model + LanguageParser SPI
|
├── ac-parser-core/ # Shared AST model + LanguageParser SPI
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
server.url=http://localhost:8787
|
server.url=http://localhost:8787
|
||||||
# Stamped by manage-ac.sh (stamp_cli_version) from ac-code-server's agenticcode.version
|
# Stamped by manage-ac.sh (stamp_cli_version) from ac-code-server's agenticcode.version
|
||||||
# at build time. "dev" means this jar wasn't built via manage-ac.sh.
|
# at build time. "dev" means this jar wasn't built via manage-ac.sh.
|
||||||
version=141
|
version=151
|
||||||
|
|||||||
@@ -54,10 +54,6 @@
|
|||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkiverse.mcp</groupId>
|
|
||||||
<artifactId>quarkus-mcp-server-sse</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jspecify</groupId>
|
<groupId>org.jspecify</groupId>
|
||||||
<artifactId>jspecify</artifactId>
|
<artifactId>jspecify</artifactId>
|
||||||
@@ -78,11 +74,6 @@
|
|||||||
<artifactId>testcontainers</artifactId>
|
<artifactId>testcontainers</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkiverse.mcp</groupId>
|
|
||||||
<artifactId>quarkus-mcp-server-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
package com.agenticcode.codeserver.mcp;
|
|
||||||
|
|
||||||
import com.agenticcode.codeserver.service.IngestSummary;
|
|
||||||
import com.agenticcode.codeserver.service.ProjectIngestService;
|
|
||||||
import com.agenticcode.codeserver.service.ProjectRootResolver;
|
|
||||||
import io.quarkiverse.mcp.server.Tool;
|
|
||||||
import io.quarkiverse.mcp.server.ToolArg;
|
|
||||||
import io.quarkiverse.mcp.server.ToolResponse;
|
|
||||||
import io.smallrye.common.annotation.Blocking;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MCP {@code refresh} tool mirroring the {@code refresh} endpoints of {@code AnalysisResource}
|
|
||||||
* (roadmap item 42). Resolves and validates the project's root folder via the shared
|
|
||||||
* {@link ProjectRootResolver} (same guards as REST: {@code PROJECT_NOT_FOUND} / {@code ROOT_NOT_SET}
|
|
||||||
* / {@code ROOT_NOT_FOUND}), then runs the corresponding {@link ProjectIngestService} refresh and
|
|
||||||
* returns the {@link IngestSummary} as JSON.
|
|
||||||
*
|
|
||||||
* <p>{@link Blocking}: refresh walks the file system and persists to Neo4j. The eager
|
|
||||||
* {@code ingest_all}/{@code ingest_module}/{@code ingest_call_graph} tools were removed — projects
|
|
||||||
* are coarse-scanned on create and deep-ingested lazily per query, with {@code refresh} as the
|
|
||||||
* explicit (re-)ingest. Destructive project CRUD (create/update/delete/clearAll) is intentionally
|
|
||||||
* <em>not</em> exposed.
|
|
||||||
*/
|
|
||||||
@ApplicationScoped
|
|
||||||
@McpLogged
|
|
||||||
public class McpIngestTools {
|
|
||||||
|
|
||||||
private final ProjectIngestService ingestService;
|
|
||||||
private final ProjectRootResolver rootResolver;
|
|
||||||
private final McpSupport support;
|
|
||||||
|
|
||||||
public McpIngestTools(ProjectIngestService ingestService, ProjectRootResolver rootResolver, McpSupport support) {
|
|
||||||
this.ingestService = ingestService;
|
|
||||||
this.rootResolver = rootResolver;
|
|
||||||
this.support = support;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "refresh", description = "Re-ingest a project from disk so the graph reflects the current files (also the remedy for a STALE_SOURCE read). With 'module', deep re-ingests that module and its dependency tree (bounded by maxDepth 1..20 and maxNodes). Without 'module', re-ingests the whole root: the call graph by default, or full field-level dataflow with deep=true.")
|
|
||||||
@Blocking
|
|
||||||
public ToolResponse refresh(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name to deep-refresh; omit to refresh the whole project", required = false) @Nullable String module,
|
|
||||||
@ToolArg(description = "Whole-project refresh only: deep (field-level) ingest", required = false) @Nullable Boolean deep,
|
|
||||||
@ToolArg(description = "Module refresh only: max dependency depth in hops (1..20); default server setting", required = false) @Nullable Integer maxDepth,
|
|
||||||
@ToolArg(description = "Module refresh only: max number of files to ingest; default server setting", required = false) @Nullable Integer maxNodes) {
|
|
||||||
if (module != null && !module.isBlank()) {
|
|
||||||
return run(project, info -> ingestService.refreshModule(info, module, maxDepth, maxNodes));
|
|
||||||
}
|
|
||||||
boolean deepIngest = deep != null && deep;
|
|
||||||
return run(project, info -> ingestService.refreshProject(info, deepIngest));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the project root, maps a validation {@link ProjectRootResolver.Failed} to a tool error,
|
|
||||||
* otherwise runs {@code ingest} and returns its {@link IngestSummary} (or an {@code INGEST_FAILED}
|
|
||||||
* tool error if the root could not be scanned).
|
|
||||||
*/
|
|
||||||
private ToolResponse run(String project, RootIngest ingest) {
|
|
||||||
return switch (rootResolver.resolve(project)) {
|
|
||||||
case ProjectRootResolver.Failed failed -> support.error(failed.code(), failed.message());
|
|
||||||
case ProjectRootResolver.Resolved resolved -> {
|
|
||||||
try {
|
|
||||||
yield support.ok(ingest.run(resolved.project()));
|
|
||||||
} catch (IOException e) {
|
|
||||||
yield support.error("INGEST_FAILED",
|
|
||||||
"Failed to scan root '" + resolved.project().root() + "': " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
private interface RootIngest {
|
|
||||||
IngestSummary run(com.agenticcode.neo4jstore.graph.ProjectInfo project) throws IOException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.agenticcode.codeserver.mcp;
|
|
||||||
|
|
||||||
import jakarta.interceptor.InterceptorBinding;
|
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds {@link McpLoggingInterceptor} to every {@code @Tool} method of the annotated class, so each
|
|
||||||
* MCP call is logged with its tool name and arguments.
|
|
||||||
*/
|
|
||||||
@InterceptorBinding
|
|
||||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
public @interface McpLogged {
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package com.agenticcode.codeserver.mcp;
|
|
||||||
|
|
||||||
import io.quarkiverse.mcp.server.Tool;
|
|
||||||
import jakarta.annotation.Priority;
|
|
||||||
import jakarta.interceptor.AroundInvoke;
|
|
||||||
import jakarta.interceptor.Interceptor;
|
|
||||||
import jakarta.interceptor.InvocationContext;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.Parameter;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs every MCP tool call ({@code @McpLogged}-annotated classes) with its tool name (from
|
|
||||||
* {@code @Tool.name()}) and argument values, so tool usage is visible in the server log the same
|
|
||||||
* way REST calls are visible via the access log.
|
|
||||||
*/
|
|
||||||
@McpLogged
|
|
||||||
@Interceptor
|
|
||||||
@Priority(Interceptor.Priority.APPLICATION)
|
|
||||||
public class McpLoggingInterceptor {
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger("com.agenticcode.mcp");
|
|
||||||
|
|
||||||
private static String formatArgs(Method method, Object[] args) {
|
|
||||||
Parameter[] params = method.getParameters();
|
|
||||||
return IntStream.range(0, args.length)
|
|
||||||
.mapToObj(i -> params[i].getName() + "=" + args[i])
|
|
||||||
.collect(Collectors.joining(", "));
|
|
||||||
}
|
|
||||||
|
|
||||||
@AroundInvoke
|
|
||||||
Object logCall(InvocationContext context) throws Exception {
|
|
||||||
Method method = context.getMethod();
|
|
||||||
Tool tool = method.getAnnotation(Tool.class);
|
|
||||||
if (tool != null) {
|
|
||||||
LOG.infof("MCP tool call: %s(%s)", tool.name(), formatArgs(method, context.getParameters()));
|
|
||||||
}
|
|
||||||
return context.proceed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,770 +0,0 @@
|
|||||||
package com.agenticcode.codeserver.mcp;
|
|
||||||
|
|
||||||
import com.agenticcode.codeserver.service.*;
|
|
||||||
import com.agenticcode.neo4jstore.graph.*;
|
|
||||||
import com.agenticcode.parsercore.ast.model.NodeType;
|
|
||||||
import io.quarkiverse.mcp.server.Tool;
|
|
||||||
import io.quarkiverse.mcp.server.ToolArg;
|
|
||||||
import io.quarkiverse.mcp.server.ToolResponse;
|
|
||||||
import io.smallrye.common.annotation.Blocking;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MCP tools mirroring the read endpoints of {@code AnalysisResource}. Each tool delegates to the
|
|
||||||
* same {@link GraphRepository} query the REST resource uses, so the graph is queried identically
|
|
||||||
* whether an agent goes through REST or MCP.
|
|
||||||
*
|
|
||||||
* <p>All tools are {@link Blocking} (they drive the blocking Neo4j driver, exactly like the
|
|
||||||
* {@code @Blocking} REST resource) and return a {@link ToolResponse} whose text content is the same
|
|
||||||
* concise JSON the REST API returns. Every module/variable/data-structure tool takes the
|
|
||||||
* {@code project} as its first argument and returns a {@code PROJECT_NOT_FOUND} tool error if it
|
|
||||||
* does not exist.
|
|
||||||
*/
|
|
||||||
@ApplicationScoped
|
|
||||||
@McpLogged
|
|
||||||
public class McpQueryTools {
|
|
||||||
|
|
||||||
private static final int DEFAULT_PAGE_LIMIT = 50;
|
|
||||||
private final GraphRepository graphRepository;
|
|
||||||
private final McpSupport support;
|
|
||||||
private final ProjectRootResolver rootResolver;
|
|
||||||
private final DeepIngestCoordinator deepIngestCoordinator;
|
|
||||||
private final SourceSnippetService sourceSnippetService;
|
|
||||||
private final SourceSearchService sourceSearchService;
|
|
||||||
private final VersionInfo versionInfo;
|
|
||||||
private final int defaultCallTreeDepth;
|
|
||||||
private final int maxCallTreeDepth;
|
|
||||||
/**
|
|
||||||
* Item 67: raw hops allowed per module hop. A pure safety cap — the query prunes its traversal to the
|
|
||||||
* modules within the requested depth, so this only stops a pathological internal chain. Measured on
|
|
||||||
* upms: a single module hop reached 9 internal hops, and the quantifier costs nothing (identical
|
|
||||||
* results and runtime at 8, 20 and 40), so it is sized well above the observed maximum. Too tight a
|
|
||||||
* value silently truncates; exhausting it sets truncated=true on the response.
|
|
||||||
*/
|
|
||||||
private final int callTreeInternalBudget;
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Projects & module discovery
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public McpQueryTools(GraphRepository graphRepository, McpSupport support,
|
|
||||||
ProjectRootResolver rootResolver, DeepIngestCoordinator deepIngestCoordinator,
|
|
||||||
SourceSnippetService sourceSnippetService, SourceSearchService sourceSearchService,
|
|
||||||
VersionInfo versionInfo,
|
|
||||||
@ConfigProperty(name = "agenticcode.call-tree.default-depth", defaultValue = "3") int defaultCallTreeDepth,
|
|
||||||
@ConfigProperty(name = "agenticcode.call-tree.max-depth", defaultValue = "10") int maxCallTreeDepth,
|
|
||||||
@ConfigProperty(name = "agenticcode.call-tree.internal-budget", defaultValue = "20") int callTreeInternalBudget) {
|
|
||||||
this.graphRepository = graphRepository;
|
|
||||||
this.support = support;
|
|
||||||
this.rootResolver = rootResolver;
|
|
||||||
this.deepIngestCoordinator = deepIngestCoordinator;
|
|
||||||
this.sourceSnippetService = sourceSnippetService;
|
|
||||||
this.sourceSearchService = sourceSearchService;
|
|
||||||
this.versionInfo = versionInfo;
|
|
||||||
this.defaultCallTreeDepth = defaultCallTreeDepth;
|
|
||||||
this.maxCallTreeDepth = maxCallTreeDepth;
|
|
||||||
this.callTreeInternalBudget = callTreeInternalBudget;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "version", description = "AgenticCode server name and version.")
|
|
||||||
public ToolResponse version() {
|
|
||||||
return support.ok(versionInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int effectiveLimit(@Nullable Integer limit) {
|
|
||||||
return limit != null ? Math.max(limit, 0) : DEFAULT_PAGE_LIMIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int effectiveOffset(@Nullable Integer offset) {
|
|
||||||
return offset != null ? Math.max(offset, 0) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item 103: see {@code AnalysisResource.uncappedLimit} — absent {@code limit} means "all rows" for
|
|
||||||
* db/workfile accesses, because their bare-array response cannot signal that it was cut.
|
|
||||||
*/
|
|
||||||
private static int uncappedLimit(@Nullable Integer limit) {
|
|
||||||
return limit != null ? Math.max(limit, 0) : Integer.MAX_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DeepIngestRequired deepIngestHint(String module, ModuleIngestState state) {
|
|
||||||
String status = state.exists() ? "NOT_DEEPLY_INGESTED" : "NOT_INGESTED";
|
|
||||||
String detail = state.exists()
|
|
||||||
? "Module '" + module + "' has only its call graph ingested; field-level dataflow requires a deep ingest."
|
|
||||||
: "Module '" + module + "' is not ingested. Ingest the call graph and/or deep-ingest it.";
|
|
||||||
return new DeepIngestRequired(status, module, detail, "ingest_module");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "list_projects", description = "List all ingested projects (name, description, root, excludeDirs, language, generatedDir, userExitDir).")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> listProjects() {
|
|
||||||
return graphRepository.listProjects().map(support::ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "list_modules", description = "List MODULE nodes (name, sourceFile, moduleKind, loc, sloc) in a project, optionally filtered by source file, moduleKind (CLASS/INTERFACE/PROGRAM/SUBPROGRAM), and/or direct EXTENDS base class name. loc = physical lines, sloc = source (non-blank, non-comment) lines; null for modules ingested before this existed.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> listModules(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Optional source file path to filter by", required = false) @Nullable String sourceFile,
|
|
||||||
@ToolArg(description = "Optional module sub-kind to filter by (CLASS/INTERFACE/PROGRAM/SUBPROGRAM)", required = false) @Nullable String moduleKind,
|
|
||||||
@ToolArg(description = "Optional base class name; lists only its direct EXTENDS subclasses", required = false) @Nullable String extendsName) {
|
|
||||||
return withProject(project, () -> graphRepository.listModules(project, sourceFile, moduleKind, extendsName).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "search_source", description = "Regex search over module SOURCE TEXT (item 54), read from disk. Returns matches as {module, sourceFile, lineNo, line}, with a 'truncated' flag when the match limit is hit. Case-insensitive by default (legacy Natural is case-insensitive). Use this to grep for code patterns (statements, table names, literals) across a project — complementary to search_identifier (which matches declared names).")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> searchSource(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Java regular expression to search for") String regex,
|
|
||||||
@ToolArg(description = "Max matches to return (default 200)", required = false) @Nullable Integer limit,
|
|
||||||
@ToolArg(description = "Case-insensitive match (default true)", required = false) @Nullable Boolean ignoreCase) {
|
|
||||||
if (regex == null || regex.isBlank()) {
|
|
||||||
return Uni.createFrom().item(support.error("MISSING_REGEX", "'regex' is required"));
|
|
||||||
}
|
|
||||||
boolean caseInsensitive = ignoreCase == null || ignoreCase;
|
|
||||||
java.util.regex.Pattern pattern;
|
|
||||||
try {
|
|
||||||
pattern = java.util.regex.Pattern.compile(regex, caseInsensitive ? java.util.regex.Pattern.CASE_INSENSITIVE : 0);
|
|
||||||
} catch (java.util.regex.PatternSyntaxException e) {
|
|
||||||
return Uni.createFrom().item(support.error("INVALID_REGEX", "Invalid regex: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
int effectiveLimit = limit != null && limit > 0 ? limit : 200;
|
|
||||||
String root;
|
|
||||||
switch (rootResolver.resolve(project)) {
|
|
||||||
case ProjectRootResolver.Resolved r -> root = r.project().root();
|
|
||||||
case ProjectRootResolver.Failed failed -> {
|
|
||||||
return Uni.createFrom().item(support.error(failed.code(), failed.message()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<ModuleInfo> modules = graphRepository.listModules(project, null, null, null).await().indefinitely();
|
|
||||||
return Uni.createFrom().item(support.ok(sourceSearchService.search(root, modules, pattern, effectiveLimit)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "project_loc", description = "Per-language LoC/SLoC rollup for a project: a breakdown by language (fileCount, loc, sloc) plus the project-wide total. loc = physical lines, sloc = source (non-blank, non-comment) lines computed per language. Each source file is counted once. For a project with a generated/user_exit split (item 47), each row also carries userExitLoc/userExitSloc and generatedExclusiveLoc/generatedExclusiveSloc (= total − user_exit); zero when unconfigured. Optionally narrow to one language ('natural'/'java') and/or one source file.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> projectLoc(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Optional language to filter by ('natural'/'java')", required = false) @Nullable String language,
|
|
||||||
@ToolArg(description = "Optional source file path to filter by", required = false) @Nullable String sourceFile) {
|
|
||||||
return withProject(project, () -> graphRepository.projectLoc(project, language, sourceFile).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "module_digest", description = "Small triage bundle for a module: description, function count, caller/callee names, DB table names, data-structure names + field counts.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> moduleDigest(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name (not a file path)") String name) {
|
|
||||||
return withProject(project, () -> graphRepository.moduleDigest(project, name).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "module_context", description = "Full context bundle for a module. Heavy sub-arrays (variableAccesses, large sqlStatements) are summarized unless requested via 'include'.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> moduleContext(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Comma-separated sections to fully expand, e.g. 'variableAccesses,sqlStatements'", required = false) @Nullable String include,
|
|
||||||
@ToolArg(description = "Max items for expanded sub-arrays (0 = unbounded)", required = false) @Nullable Integer limit,
|
|
||||||
@ToolArg(description = "Offset into expanded sub-arrays", required = false) @Nullable Integer offset) {
|
|
||||||
int effLimit = limit != null ? Math.max(limit, 0) : 0;
|
|
||||||
int effOffset = offset != null ? Math.max(offset, 0) : 0;
|
|
||||||
return withProject(project, () -> graphRepository.moduleContext(project, name, include, effLimit, effOffset).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Call graph
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@Tool(name = "module_functions", description = "List functions (Natural subroutines / Java methods) declared in a module; set includeInherited to include inherited Java methods. 'kind' filters Java methods by modifier: abstract/final/overridable.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> moduleFunctions(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Include inherited (Java) methods", required = false) @Nullable Boolean includeInherited,
|
|
||||||
@ToolArg(description = "Optional Java modifier filter: abstract/final/overridable", required = false) @Nullable String kind) {
|
|
||||||
boolean inherited = includeInherited != null && includeInherited;
|
|
||||||
return withProject(project, () -> graphRepository.moduleFunctions(project, name, inherited, kind).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "function_overrides", description = "Concrete subclass overrides of a base-class method (Java virtual/template-method dispatch). Pass 'function' for one hook method, or omit it to get overrides for every abstract method of the base class at once, grouped by hook method.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> functionOverrides(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module (base class) name") String name,
|
|
||||||
@ToolArg(description = "Base method name; omit for the bulk (all-abstract-methods) variant", required = false) @Nullable String function) {
|
|
||||||
return withProject(project, () -> function == null || function.isBlank()
|
|
||||||
? graphRepository.functionOverrides(project, name).map(support::ok)
|
|
||||||
: graphRepository.functionOverrides(project, name, function).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "function_callers", description = "FUNCTION-level callers of a subroutine/method: who PERFORMs (Natural) or calls (Java cross-class) the given 'function' in module 'name', with call-site line numbers. Finer-grained than module-level 'callers'.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> functionCallers(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name that defines the subroutine/method") String name,
|
|
||||||
@ToolArg(description = "Subroutine/method name") String function) {
|
|
||||||
return withProject(project, () -> graphRepository.functionCallers(project, name, function).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "module_data_structures", description = "Data structures (Natural DEFINE DATA blocks / Java DTOs) referenced by a module.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> moduleDataStructures(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name) {
|
|
||||||
return withProject(project, () -> graphRepository.moduleDataStructures(project, name).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "module_payload", description = "XML wire payload contract of a Natural module: the tag/field/direction triples emitted via the ADD-XML-LINE idiom.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> modulePayload(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name) {
|
|
||||||
return withProject(project, () -> graphRepository.payload(project, name).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "module_dispatch_table", description = "Dispatch table for a module: dynamic-call lookup arrays and the module names their literals resolve to.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> moduleDispatchTable(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name) {
|
|
||||||
return withProject(project, () -> graphRepository.dispatchTable(project, name).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Dynamic-CALLNAT manual overrides (item 82)
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@Tool(name = "list_unresolved_dynamic_calls", description = "List the unresolved dynamic CALLNAT <var> call sites in a project — the ones the auto-resolvers couldn't recover. Each row is {module, originFile, lineNo, variable}; (originFile, lineNo) is the key to pin a target with set_dynamic_call_override.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> listUnresolvedDynamicCalls(
|
|
||||||
@ToolArg(description = "Project name") String project) {
|
|
||||||
return withProject(project, () -> graphRepository.listUnresolvedDynamicCalls(project).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "list_dynamic_call_overrides", description = "List the manual dynamic-CALLNAT overrides in a project. Each carries its targets and an 'obsolete' flag (true once an auto-resolver has since resolved that call site).")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> listDynamicCallOverrides(
|
|
||||||
@ToolArg(description = "Project name") String project) {
|
|
||||||
return withProject(project, () -> graphRepository.listDynamicCallOverrides(project).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "set_dynamic_call_override", description = "Pin an unresolvable dynamic CALLNAT call site (identified by originFile + lineNo) to one or more target modules. Multiple targets model deliberate branches. Applied immediately (no refresh) and persisted across refreshes. Rejected if any target is not a real module.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> setDynamicCallOverride(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "File the call site lives in (module's own file or an included copycode)") String originFile,
|
|
||||||
@ToolArg(description = "Line number of the CALLNAT within originFile") Integer lineNo,
|
|
||||||
@ToolArg(description = "Comma-separated target module name(s), e.g. 'YGEAGGN0' or 'A,B'") String targets,
|
|
||||||
@ToolArg(description = "Dispatch variable name, for reference (optional)", required = false) @Nullable String variable,
|
|
||||||
@ToolArg(description = "Free-text note (optional)", required = false) @Nullable String note) {
|
|
||||||
List<String> targetList = Arrays.stream(targets.split(","))
|
|
||||||
.map(String::trim).filter(s -> !s.isEmpty()).distinct().toList();
|
|
||||||
if (originFile.isBlank() || lineNo == null || lineNo <= 0 || targetList.isEmpty()) {
|
|
||||||
return Uni.createFrom().item(support.error("INVALID_REQUEST",
|
|
||||||
"originFile, a positive lineNo and at least one target are required"));
|
|
||||||
}
|
|
||||||
return withProject(project, () -> graphRepository.upsertDynamicCallOverride(project, originFile, lineNo,
|
|
||||||
targetList, variable, note, "mcp")
|
|
||||||
.map(result -> {
|
|
||||||
DynamicCallOverride ov = result.override();
|
|
||||||
return ov != null
|
|
||||||
? support.ok(ov)
|
|
||||||
: support.error("UNKNOWN_TARGET",
|
|
||||||
"not real MODULE(s) in project '" + project + "': " + result.invalidTargets());
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "reset_dynamic_call_override", description = "Remove manual dynamic-CALLNAT overrides and restore the unresolved placeholder inline: pass originFile + lineNo for one call site, or omit both to reset every override in the project.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> resetDynamicCallOverride(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Call site file; omit (with lineNo) to reset all", required = false) @Nullable String originFile,
|
|
||||||
@ToolArg(description = "Call site line; omit (with originFile) to reset all", required = false) @Nullable Integer lineNo) {
|
|
||||||
return withProject(project, () -> graphRepository.resetDynamicCallOverride(project, originFile, lineNo)
|
|
||||||
.map(removed -> support.ok(Map.of("removed", removed))));
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// DB access
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@Tool(name = "module_columns", description = "Entity/table columns exposed by a module that maps to a DB table (e.g. a JPA @Entity).")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> moduleColumns(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name) {
|
|
||||||
return withProject(project, () -> graphRepository.entityColumns(project, name).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Collection<String> stepModules(List<DataflowStep> steps) {
|
|
||||||
return steps.stream().map(DataflowStep::module).filter(Objects::nonNull).distinct().toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "callees", description = "Modules/functions the given module calls (plus Java INJECTS/REFERENCES wiring). 'scope' filters by edge kind; resolveInterfaces hops interface callees to their implementations.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> callees(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Optional edge-kind scope filter", required = false) @Nullable String scope,
|
|
||||||
@ToolArg(description = "Page size (default 50)", required = false) @Nullable Integer limit,
|
|
||||||
@ToolArg(description = "Page offset (default 0)", required = false) @Nullable Integer offset,
|
|
||||||
@ToolArg(description = "Hop interface callees to their implementation(s) (Java)", required = false) @Nullable Boolean resolveInterfaces) {
|
|
||||||
return withProject(project, () -> graphRepository.callees(project, name, scope, effectiveLimit(limit), effectiveOffset(offset),
|
|
||||||
resolveInterfaces != null && resolveInterfaces).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Collection<String> fieldFlowModules(List<FieldFlow> flows) {
|
|
||||||
Set<String> names = new LinkedHashSet<>();
|
|
||||||
for (FieldFlow flow : flows) {
|
|
||||||
names.add(flow.producer());
|
|
||||||
names.add(flow.consumer());
|
|
||||||
}
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Search
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@Tool(name = "db_accesses", description = "DB tables a module reads/writes and the access mode. With 'depth' > 0, includes accesses of transitively-called modules.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> dbAccesses(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Transitive depth (0/absent = direct only)", required = false) @Nullable Integer depth,
|
|
||||||
@ToolArg(description = "Page size (default: all rows)", required = false) @Nullable Integer limit,
|
|
||||||
@ToolArg(description = "Page offset (default 0)", required = false) @Nullable Integer offset) {
|
|
||||||
int effLimit = uncappedLimit(limit);
|
|
||||||
int effOffset = effectiveOffset(offset);
|
|
||||||
return withProject(project, () -> depth != null && depth > 0
|
|
||||||
? graphRepository.dbAccessesTransitive(project, name, depth, effLimit, effOffset).map(support::ok)
|
|
||||||
: graphRepository.dbAccesses(project, name, effLimit, effOffset).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "workfile_accesses", description = "Natural work files (sequential/flat file I/O) a module reads/writes: work-file number, mode (READS/WRITES), record buffers, and physical name from DEFINE WORK FILE. Separate from db_accesses (work files are not DB tables).")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> workfileAccesses(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Page size (default: all rows)", required = false) @Nullable Integer limit,
|
|
||||||
@ToolArg(description = "Page offset (default 0)", required = false) @Nullable Integer offset) {
|
|
||||||
int effLimit = uncappedLimit(limit);
|
|
||||||
int effOffset = effectiveOffset(offset);
|
|
||||||
return withProject(project, () -> graphRepository.workfileAccesses(project, name, effLimit, effOffset).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "sql_statements", description = "SQL/ADABAS statements issued by a module. With 'depth' > 0, includes statements of transitively-called modules.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> sqlStatements(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Transitive depth (0/absent = direct only)", required = false) @Nullable Integer depth) {
|
|
||||||
return withProject(project, () -> depth != null && depth > 0
|
|
||||||
? graphRepository.sqlStatementsTransitive(project, name, depth).map(support::ok)
|
|
||||||
: graphRepository.sqlStatements(project, name).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Variable access & dataflow
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@Tool(name = "data_structure_fields", description = "Fields of a data structure (Natural DEFINE DATA / Java DTO): name, type, level, redefines. Each field carries its sourceFile.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> dataStructureFields(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Data-structure name") String name,
|
|
||||||
@ToolArg(description = "Restrict to the definition in this source file (only needed when the name is not unique)",
|
|
||||||
required = false) @Nullable String sourceFile) {
|
|
||||||
return withProject(project, () -> graphRepository.dataStructureFields(project, name, sourceFile).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "db_table_columns", description = "Columns of a DB table (ADABAS view / SQL table).")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> dbTableColumns(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "DB table name") String name) {
|
|
||||||
return withProject(project, () -> graphRepository.dbTableColumns(project, name).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "callers", description = "Callers of the given module. Default/'external' = modules that call it (CALLNAT/inheritance), each rolled up to the calling module (a call from inside a subroutine is attributed to its module, not the FUNCTION; repeated sites collapse to one row); 'internal' = its own subroutines' PERFORM wiring. Finer function-level callers: function_callers.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> callers(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Scope: 'external' (default) = module callers; 'internal' = own-subroutine PERFORM callers", required = false) @Nullable String scope,
|
|
||||||
@ToolArg(description = "Page size (default 50)", required = false) @Nullable Integer limit,
|
|
||||||
@ToolArg(description = "Page offset (default 0)", required = false) @Nullable Integer offset) {
|
|
||||||
return withFanoutWarm(project,
|
|
||||||
() -> graphRepository.callers(project, name, scope, effectiveLimit(limit), effectiveOffset(offset)),
|
|
||||||
CallRefResponse::sourceFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "search_value", description = "Find nodes whose literal/assigned value matches the given string across a project.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> searchValue(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Value to search for") String value,
|
|
||||||
@ToolArg(description = "If true, case-insensitive substring match instead of exact", required = false) @Nullable Boolean contains,
|
|
||||||
@ToolArg(description = "Page size (default 50)", required = false) @Nullable Integer limit,
|
|
||||||
@ToolArg(description = "Page offset (default 0)", required = false) @Nullable Integer offset) {
|
|
||||||
if (value.isBlank()) {
|
|
||||||
return Uni.createFrom().item(support.error("MISSING_VALUE", "Argument 'value' is required"));
|
|
||||||
}
|
|
||||||
return withProject(project, () -> graphRepository.searchByValue(project, value, Boolean.TRUE.equals(contains),
|
|
||||||
effectiveLimit(limit), effectiveOffset(offset)).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "search_annotation", description = "Find nodes (Java only) carrying an annotation whose name matches (substring, case-insensitive), optionally filtered by node type.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> searchAnnotation(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Annotation name / substring") String name,
|
|
||||||
@ToolArg(description = "Optional node type filter", required = false) @Nullable String type,
|
|
||||||
@ToolArg(description = "Page size (default 50)", required = false) @Nullable Integer limit,
|
|
||||||
@ToolArg(description = "Page offset (default 0)", required = false) @Nullable Integer offset) {
|
|
||||||
if (name.isBlank()) {
|
|
||||||
return Uni.createFrom().item(support.error("MISSING_NAME", "Argument 'name' is required"));
|
|
||||||
}
|
|
||||||
if (type != null) {
|
|
||||||
try {
|
|
||||||
NodeType.valueOf(type.toUpperCase());
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return Uni.createFrom().item(support.error("INVALID_TYPE", "Unknown node type '" + type + "'"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return withProject(project, () -> graphRepository.searchAnnotation(project, name,
|
|
||||||
type != null ? type.toUpperCase() : null, effectiveLimit(limit), effectiveOffset(offset)).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "inspect_node", description = "Every property of a single node by id (roadmap item 27). Node ids are regenerated on re-ingest, so only valid until the next one.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> inspectNode(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Node id") String id) {
|
|
||||||
return withProject(project, () -> graphRepository.nodeById(project, id).map(node -> node == null
|
|
||||||
? support.error("NODE_NOT_FOUND", "No node with id '" + id + "' in project '" + project + "' (ids are regenerated on re-ingest)")
|
|
||||||
: support.ok(node)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "call_tree", description = "Transitive call tree rooted at a module, up to 'depth' hops (clamped to the configured maximum). resolveInterfaces drops dead-end interface nodes whose implementations are already reached. followWiring also traverses Java INJECTS/REFERENCES edges (DI/class-literal wiring), not just CALLS.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> callTree(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Traversal depth (default configured, clamped 1..max)", required = false) @Nullable Integer depth,
|
|
||||||
@ToolArg(description = "Drop interface nodes that have a known implementation (Java)", required = false) @Nullable Boolean resolveInterfaces,
|
|
||||||
@ToolArg(description = "Also traverse INJECTS/REFERENCES edges (Java DI/class-literal wiring), not just CALLS", required = false) @Nullable Boolean followWiring) {
|
|
||||||
int effectiveDepth = Math.clamp(depth != null ? depth : defaultCallTreeDepth, 1, maxCallTreeDepth);
|
|
||||||
return withFanoutWarm(project,
|
|
||||||
() -> graphRepository.callTree(project, name, effectiveDepth,
|
|
||||||
resolveInterfaces != null && resolveInterfaces, followWiring != null && followWiring,
|
|
||||||
callTreeInternalBudget),
|
|
||||||
CallTreeResponse::sourceFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "ego_graph", description = "Bounded call-graph neighbourhood (nodes + edges) around one module, at module granularity (item 49). Unlike call_tree it returns the edges too, so a caller can render an interactive sub-graph. 'direction' is out (callees), in (callers), or both; 'depth' is the max call hops (clamped to the configured maximum); 'limit' caps the number of nodes (BFS order) and sets 'truncated' when hit. Unresolved dynamic/cross-file targets appear as nodes with an empty sourceFile and unresolved=true.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> egoGraph(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Max call hops from the module (default configured, clamped 1..max)", required = false) @Nullable Integer depth,
|
|
||||||
@ToolArg(description = "Traversal direction: out (callees), in (callers), or both (default out)", required = false) @Nullable String direction,
|
|
||||||
@ToolArg(description = "Cap on number of nodes returned (default 50, BFS order)", required = false) @Nullable Integer limit) {
|
|
||||||
int effectiveDepth = Math.clamp(depth != null ? depth : defaultCallTreeDepth, 1, maxCallTreeDepth);
|
|
||||||
String dir = direction != null ? direction : "out";
|
|
||||||
int effectiveLimit = effectiveLimit(limit);
|
|
||||||
return withProject(project, () -> graphRepository.egoGraph(project, name, effectiveDepth, dir, effectiveLimit)
|
|
||||||
.map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "search_identifier", description = "Find identifiers across a project by exact name and optional node type (MODULE, FUNCTION, VARIABLE, DATA_STRUCTURE, DB_TABLE). Name match is sigil-insensitive: a leading Natural sigil (# user, & AIV, + GDA) is ignored on both sides, so 'K-OUT-MAX' matches the declared '#K-OUT-MAX'. Optionally scope to one source file or one module (by name) to pinpoint the local declaration when a name recurs across modules; or use priorityModule to keep the global list but pin one module's matches to the front so they survive the limit.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> searchIdentifier(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Identifier name", required = false) @Nullable String name,
|
|
||||||
@ToolArg(description = "Optional node type filter", required = false) @Nullable String type,
|
|
||||||
@ToolArg(description = "Scope to this exact source file (relative path)", required = false) @Nullable String sourceFile,
|
|
||||||
@ToolArg(description = "Scope to nodes of this module (by name)", required = false) @Nullable String module,
|
|
||||||
@ToolArg(description = "Do not filter, but pin this module's matches to the front so its local declaration survives the limit when a name recurs across many modules", required = false) @Nullable String priorityModule,
|
|
||||||
@ToolArg(description = "Page size (default 50)", required = false) @Nullable Integer limit,
|
|
||||||
@ToolArg(description = "Page offset (default 0)", required = false) @Nullable Integer offset) {
|
|
||||||
if (type != null) {
|
|
||||||
try {
|
|
||||||
NodeType.valueOf(type.toUpperCase());
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return Uni.createFrom().item(support.error("INVALID_TYPE", "Unknown node type '" + type + "'"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return withFanoutWarm(project,
|
|
||||||
() -> graphRepository.searchIdentifier(project, name,
|
|
||||||
type != null ? type.toUpperCase() : null, sourceFile, module, priorityModule, effectiveLimit(limit), effectiveOffset(offset)),
|
|
||||||
matches -> matches.stream().map(IdentifierMatch::sourceFile).filter(sf -> !sf.isEmpty()).distinct().toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "node_source", description = "Source-text snippet for a single node by id (roadmap item 28), read from the project root on disk.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> nodeSource(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Node id") String id) {
|
|
||||||
return withProject(project, () -> graphRepository.nodeById(project, id).flatMap(node -> {
|
|
||||||
if (node == null) {
|
|
||||||
return Uni.createFrom().item(support.error("NODE_NOT_FOUND", "No node with id '" + id + "' in project '" + project + "' (ids are regenerated on re-ingest)"));
|
|
||||||
}
|
|
||||||
String sourceFile = (String) node.get("sourceFile");
|
|
||||||
if (sourceFile == null || sourceFile.isEmpty()) {
|
|
||||||
return Uni.createFrom().item(support.error("NO_SOURCE_FILE", "Node '" + id + "' is an unresolved placeholder with no source file"));
|
|
||||||
}
|
|
||||||
int startLine = ((Number) Objects.requireNonNull(node.get("startLine"))).intValue();
|
|
||||||
int endLine = ((Number) Objects.requireNonNull(node.get("endLine"))).intValue();
|
|
||||||
return sourceSnippet(project, sourceFile, startLine, endLine);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "module_source", description = "Source text for a module by name (roadmap item 28), read from the project root on disk. Omit both startLine and endLine to get the whole file (M1); or pass both for a line range.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> moduleSource(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Module name") String name,
|
|
||||||
@ToolArg(description = "Start line (1-indexed); omit with endLine for the whole file", required = false) @Nullable Integer startLine,
|
|
||||||
@ToolArg(description = "End line (1-indexed, inclusive); omit with startLine for the whole file", required = false) @Nullable Integer endLine) {
|
|
||||||
boolean wholeFile = startLine == null && endLine == null;
|
|
||||||
if (!wholeFile && (startLine == null || endLine == null)) {
|
|
||||||
return Uni.createFrom().item(support.error("MISSING_LINE_RANGE",
|
|
||||||
"Supply both startLine and endLine, or neither (whole file)"));
|
|
||||||
}
|
|
||||||
int sl = startLine != null ? startLine : 1;
|
|
||||||
int el = endLine != null ? endLine : Integer.MAX_VALUE;
|
|
||||||
return withProject(project, () -> graphRepository.moduleSourceFile(project, name).flatMap(sourceFile -> sourceFile == null
|
|
||||||
? Uni.createFrom().item(support.error("MODULE_NOT_FOUND", "No module '" + name + "' in project '" + project + "'"))
|
|
||||||
: wholeFile
|
|
||||||
? sourceSnippetWhole(project, sourceFile)
|
|
||||||
: sourceSnippet(project, sourceFile, sl, el)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "file_source", description = "Source text for a project file by its relative path (not a module name), read from the project root on disk. Use for files that aren't standalone modules — e.g. a Natural data area (PDA/LDA) USING'd by a module, whose fields' line numbers refer to that file. Omit both startLine and endLine to get the whole file; or pass both for a line range.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> fileSource(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Relative source file path (as reported by e.g. module_data_structures.sourceFile)") String file,
|
|
||||||
@ToolArg(description = "Start line (1-indexed); omit with endLine for the whole file", required = false) @Nullable Integer startLine,
|
|
||||||
@ToolArg(description = "End line (1-indexed, inclusive); omit with startLine for the whole file", required = false) @Nullable Integer endLine) {
|
|
||||||
if (file == null || file.isBlank()) {
|
|
||||||
return Uni.createFrom().item(support.error("MISSING_FILE", "'file' (relative source path) is required"));
|
|
||||||
}
|
|
||||||
boolean wholeFile = startLine == null && endLine == null;
|
|
||||||
if (!wholeFile && (startLine == null || endLine == null)) {
|
|
||||||
return Uni.createFrom().item(support.error("MISSING_LINE_RANGE",
|
|
||||||
"Supply both startLine and endLine, or neither (whole file)"));
|
|
||||||
}
|
|
||||||
int sl = startLine != null ? startLine : 1;
|
|
||||||
int el = endLine != null ? endLine : Integer.MAX_VALUE;
|
|
||||||
return withProject(project, () -> wholeFile ? sourceSnippetWhole(project, file) : sourceSnippet(project, file, sl, el));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "variable_reads", description = "Functions that read a given variable, following the call graph up to 'depth' hops.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> variableReads(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Variable name") String name,
|
|
||||||
@ToolArg(description = "Optional module to scope to", required = false) @Nullable String module,
|
|
||||||
@ToolArg(description = "Traversal depth (default configured, clamped 1..max)", required = false) @Nullable Integer depth) {
|
|
||||||
int effectiveDepth = Math.clamp(depth != null ? depth : defaultCallTreeDepth, 1, maxCallTreeDepth);
|
|
||||||
return withProject(project, () -> graphRepository.variableReads(project, name, module, effectiveDepth).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Helpers (mirror AnalysisResource)
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@Tool(name = "variable_writes", description = "Functions that write a given variable, following the call graph up to 'depth' hops.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> variableWrites(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Variable name") String name,
|
|
||||||
@ToolArg(description = "Optional module to scope to", required = false) @Nullable String module,
|
|
||||||
@ToolArg(description = "Traversal depth (default configured, clamped 1..max)", required = false) @Nullable Integer depth) {
|
|
||||||
int effectiveDepth = Math.clamp(depth != null ? depth : defaultCallTreeDepth, 1, maxCallTreeDepth);
|
|
||||||
return withProject(project, () -> graphRepository.variableWrites(project, name, module, effectiveDepth).map(support::ok));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks up {@code sourceFile}'s stored content hash (item 41), then reads its {@code [startLine,
|
|
||||||
* endLine]} slice with a stale check.
|
|
||||||
*/
|
|
||||||
private Uni<ToolResponse> sourceSnippet(String project, String sourceFile, int startLine, int endLine) {
|
|
||||||
return graphRepository.sourceHash(project, sourceFile)
|
|
||||||
.map(expectedHash -> readSnippet(project, sourceFile,
|
|
||||||
(root, hash) -> sourceSnippetService.read(root, sourceFile, startLine, endLine, hash), expectedHash));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* M1: whole-file variant of {@link #sourceSnippet}.
|
|
||||||
*/
|
|
||||||
private Uni<ToolResponse> sourceSnippetWhole(String project, String sourceFile) {
|
|
||||||
return graphRepository.sourceHash(project, sourceFile)
|
|
||||||
.map(expectedHash -> readSnippet(project, sourceFile,
|
|
||||||
(root, hash) -> sourceSnippetService.readWholeFile(root, sourceFile, hash), expectedHash));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the project root and runs {@code reader} against it, mapping root-resolution/IO
|
|
||||||
* failures to MCP tool errors. When the file on disk no longer matches the stored hash (item 41),
|
|
||||||
* returns a {@code STALE_SOURCE} error telling the caller to re-ingest, rather than serving
|
|
||||||
* changed text against stale line numbers.
|
|
||||||
*/
|
|
||||||
private ToolResponse readSnippet(String project, String sourceFile, SnippetReader reader,
|
|
||||||
@Nullable String expectedHash) {
|
|
||||||
String root;
|
|
||||||
switch (rootResolver.resolve(project)) {
|
|
||||||
case ProjectRootResolver.Resolved r -> root = r.project().root();
|
|
||||||
case ProjectRootResolver.Failed failed -> {
|
|
||||||
return support.error(failed.code(), failed.message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Defence in depth: file_source takes a client-supplied path, so reject any that escapes root.
|
|
||||||
Path base = Path.of(root).toAbsolutePath().normalize();
|
|
||||||
if (!base.resolve(sourceFile).normalize().startsWith(base)) {
|
|
||||||
return support.error("INVALID_SOURCE_FILE", "Source file '" + sourceFile + "' escapes the project root");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return support.ok(reader.read(root, expectedHash));
|
|
||||||
} catch (StaleSourceException e) {
|
|
||||||
return support.error("STALE_SOURCE", e.detail());
|
|
||||||
} catch (IOException e) {
|
|
||||||
return support.error("SOURCE_READ_FAILED", "Failed to read '" + sourceFile + "': " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
private interface SnippetReader {
|
|
||||||
SourceSnippet read(String root, @Nullable String expectedHash) throws IOException, StaleSourceException;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "flow_forward", description = "Forward field-level dataflow from a variable (where its value flows to). Requires the module to be deep-ingested.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> flowForward(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Variable name") String name,
|
|
||||||
@ToolArg(description = "Module the variable lives in", required = false) @Nullable String module,
|
|
||||||
@ToolArg(description = "Traversal depth (default configured)", required = false) @Nullable Integer depth) {
|
|
||||||
int effectiveDepth = depth != null ? depth : defaultCallTreeDepth;
|
|
||||||
return withFlowPathWarm(project, module,
|
|
||||||
() -> graphRepository.flowForward(project, name, module, effectiveDepth),
|
|
||||||
McpQueryTools::stepModules);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs {@code action} if the project exists, otherwise a {@code PROJECT_NOT_FOUND} tool error.
|
|
||||||
*/
|
|
||||||
private Uni<ToolResponse> withProject(String project, Supplier<Uni<ToolResponse>> action) {
|
|
||||||
return graphRepository.projectExists(project).flatMap(exists -> exists
|
|
||||||
? action.get()
|
|
||||||
: Uni.createFrom().item(support.error("PROJECT_NOT_FOUND", "Project '" + project + "' does not exist")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "flow_backward", description = "Backward field-level dataflow into a variable (where its value comes from). Requires the module to be deep-ingested.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> flowBackward(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Variable name") String name,
|
|
||||||
@ToolArg(description = "Module the variable lives in", required = false) @Nullable String module,
|
|
||||||
@ToolArg(description = "Traversal depth (default configured)", required = false) @Nullable Integer depth) {
|
|
||||||
int effectiveDepth = depth != null ? depth : defaultCallTreeDepth;
|
|
||||||
return withFlowPathWarm(project, module,
|
|
||||||
() -> graphRepository.flowBackward(project, name, module, effectiveDepth),
|
|
||||||
McpQueryTools::stepModules);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Tool(name = "field_flow", description = "Producer/consumer field-flow pairs for a variable within its module. Requires the module to be deep-ingested.")
|
|
||||||
@Blocking
|
|
||||||
public Uni<ToolResponse> fieldFlow(
|
|
||||||
@ToolArg(description = "Project name") String project,
|
|
||||||
@ToolArg(description = "Variable name") String name,
|
|
||||||
@ToolArg(description = "Module the variable lives in", required = false) @Nullable String module,
|
|
||||||
@ToolArg(description = "Traversal depth (default configured)", required = false) @Nullable Integer depth) {
|
|
||||||
int effectiveDepth = depth != null ? depth : defaultCallTreeDepth;
|
|
||||||
return withFlowPathWarm(project, module,
|
|
||||||
() -> graphRepository.fieldFlow(project, name, module, effectiveDepth),
|
|
||||||
McpQueryTools::fieldFlowModules);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fan-out / traversal deep-ingest warm (item 37, blocking-then-rerun): runs {@code query} against
|
|
||||||
* the graph as-is, deep-ingests the modules the result surfaced ({@code surfaced}) via
|
|
||||||
* {@link DeepIngestCoordinator#ensureDeepMany}, and — only if that warm actually deepened
|
|
||||||
* something — re-runs {@code query} against the now-deeper graph so the response reflects
|
|
||||||
* newly-resolved dynamic dispatch. When nothing changed (already deep, empty set, or the warm
|
|
||||||
* failed) the first result is returned directly, so the query never pays for a redundant re-run.
|
|
||||||
*
|
|
||||||
* @param surfaced maps a result to the {@code sourceFile} paths it surfaced (the warm target)
|
|
||||||
*/
|
|
||||||
private <T> Uni<ToolResponse> withFanoutWarm(String project, Supplier<Uni<T>> query,
|
|
||||||
Function<T, Collection<String>> surfaced) {
|
|
||||||
return withProject(project, () -> query.get().flatMap(first ->
|
|
||||||
deepIngestCoordinator.ensureDeepMany(project, surfaced.apply(first)).flatMap(changed ->
|
|
||||||
changed ? query.get().map(support::ok) : Uni.createFrom().item(support.ok(first)))));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cross-module {@code flow_*} path-ingest (item 37a), the MCP counterpart of
|
|
||||||
* {@code AnalysisResource.withFlowPathWarm}: deep-ingests the named start {@code module} (falling
|
|
||||||
* back to the agent-actionable deep-ingest hint if it does not resolve to {@code FULL}), then runs
|
|
||||||
* {@code query} and drives a bounded ingest-and-re-traverse fixpoint via {@link #flowFixpoint} —
|
|
||||||
* each round deep-ingests the frontier the current result surfaced
|
|
||||||
* ({@link DeepIngestCoordinator#ensureFlowFrontier}) and re-runs the trace, so the flow crosses
|
|
||||||
* into callees (notably dynamically-dispatched ones) as they become deeply ingested. With no
|
|
||||||
* {@code module} it is a plain project-wide query.
|
|
||||||
*/
|
|
||||||
private <T> Uni<ToolResponse> withFlowPathWarm(String project, @Nullable String module,
|
|
||||||
Supplier<Uni<List<T>>> query,
|
|
||||||
Function<List<T>, Collection<String>> surfaced) {
|
|
||||||
if (module == null || module.isBlank()) {
|
|
||||||
return withProject(project, () -> query.get().map(support::ok));
|
|
||||||
}
|
|
||||||
String startModule = module;
|
|
||||||
return withProject(project, () -> deepIngestCoordinator.ensureDeep(project, startModule)
|
|
||||||
.flatMap(ignored -> graphRepository.moduleIngestState(project, startModule).flatMap(state ->
|
|
||||||
state.isFull()
|
|
||||||
? flowFixpoint(project, startModule, query, surfaced, deepIngestCoordinator.flowRounds())
|
|
||||||
: Uni.createFrom().item(support.errorBody(deepIngestHint(startModule, state))))));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One iteration of the flow path-ingest fixpoint: runs {@code query}, and while rounds remain and
|
|
||||||
* {@link DeepIngestCoordinator#ensureFlowFrontier} reports it deepened the frontier, re-traverses;
|
|
||||||
* otherwise returns the current result. The frontier seed always includes the start {@code module}
|
|
||||||
* (not just what the trace surfaced) so the first round pulls in the start module's not-yet-deep —
|
|
||||||
* notably dynamically-dispatched — callees even when the trace is still empty. Bounded by
|
|
||||||
* {@code roundsLeft} (configured {@code flow-rounds}) so it terminates even if the graph keeps
|
|
||||||
* surfacing new callees.
|
|
||||||
*/
|
|
||||||
private <T> Uni<ToolResponse> flowFixpoint(String project, String module, Supplier<Uni<List<T>>> query,
|
|
||||||
Function<List<T>, Collection<String>> surfaced, int roundsLeft) {
|
|
||||||
return query.get().flatMap(result -> {
|
|
||||||
if (roundsLeft <= 0) {
|
|
||||||
return Uni.createFrom().item(support.ok(result));
|
|
||||||
}
|
|
||||||
Set<String> frontier = new LinkedHashSet<>();
|
|
||||||
frontier.add(module);
|
|
||||||
frontier.addAll(surfaced.apply(result));
|
|
||||||
return deepIngestCoordinator.ensureFlowFrontier(project, frontier).flatMap(changed ->
|
|
||||||
changed
|
|
||||||
? flowFixpoint(project, module, query, surfaced, roundsLeft - 1)
|
|
||||||
: Uni.createFrom().item(support.ok(result)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agent-actionable hint: a deep query needs the module (deep-)ingested first via the named MCP tool.
|
|
||||||
*/
|
|
||||||
public record DeepIngestRequired(String status, String module, String detail, String nextTool) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package com.agenticcode.codeserver.mcp;
|
|
||||||
|
|
||||||
import com.agenticcode.codeserver.api.ErrorResponse;
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import io.quarkiverse.mcp.server.ToolResponse;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes MCP tool results to the same concise JSON the REST API emits, so an agent gets an
|
|
||||||
* identical payload shape whether it calls a tool or an endpoint.
|
|
||||||
*
|
|
||||||
* <p>Success results become a JSON text content; error results reuse the REST
|
|
||||||
* {@link ErrorResponse} shape ({@code {"error","code","details"}}) and are flagged as MCP tool
|
|
||||||
* errors ({@code isError=true}).
|
|
||||||
*/
|
|
||||||
@ApplicationScoped
|
|
||||||
class McpSupport {
|
|
||||||
|
|
||||||
private final ObjectMapper mapper;
|
|
||||||
|
|
||||||
McpSupport(ObjectMapper mapper) {
|
|
||||||
this.mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Success response carrying {@code value} serialized as JSON text.
|
|
||||||
*/
|
|
||||||
ToolResponse ok(Object value) {
|
|
||||||
return ToolResponse.success(toJson(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error response with the REST {@link ErrorResponse} JSON shape and {@code isError=true}.
|
|
||||||
*/
|
|
||||||
ToolResponse error(String code, String message) {
|
|
||||||
return ToolResponse.error(toJson(new ErrorResponse(message, code, Map.of())));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error response carrying an already-built structured body (e.g. a deep-ingest hint).
|
|
||||||
*/
|
|
||||||
ToolResponse errorBody(Object body) {
|
|
||||||
return ToolResponse.error(toJson(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String toJson(Object value) {
|
|
||||||
try {
|
|
||||||
return mapper.writeValueAsString(value);
|
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
// Serialization of our own DTOs should never fail; surface loudly if it ever does.
|
|
||||||
throw new IllegalStateException("MCP JSON serialization failed for " + value.getClass(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/**
|
|
||||||
* MCP tool endpoints for AI agent integration.
|
|
||||||
*/
|
|
||||||
@org.jspecify.annotations.NullMarked
|
|
||||||
package com.agenticcode.codeserver.mcp;
|
|
||||||
@@ -10,10 +10,10 @@ import java.nio.file.Path;
|
|||||||
/**
|
/**
|
||||||
* Loads a project and validates that its root folder is usable for ingest.
|
* Loads a project and validates that its root folder is usable for ingest.
|
||||||
*
|
*
|
||||||
* <p>Shared by the REST {@code AnalysisResource} and the MCP ingest tools so the guard logic
|
* <p>Shared by the REST resources so the guard logic
|
||||||
* (project exists, root configured, root is a directory on the server) has a single source of
|
* (project exists, root configured, root is a directory on the server) has a single source of
|
||||||
* truth and cannot drift between the two entry points. The result carries a transport-neutral
|
* truth and cannot drift between the two entry points. The result carries a transport-neutral
|
||||||
* {@code code}; each caller maps it to its own error shape (HTTP status / MCP tool error).
|
* {@code code}; each caller maps it to its own error shape (HTTP status).
|
||||||
*/
|
*/
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class ProjectRootResolver {
|
public class ProjectRootResolver {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
|
|||||||
* Single source of truth for the server's product name/version — {@code version} is
|
* Single source of truth for the server's product name/version — {@code version} is
|
||||||
* {@code agenticcode.version} (a manually-bumped release counter in {@code application.properties},
|
* {@code agenticcode.version} (a manually-bumped release counter in {@code application.properties},
|
||||||
* deliberately independent of the Maven project version). Shared by the startup log line
|
* deliberately independent of the Maven project version). Shared by the startup log line
|
||||||
* ({@link VersionLogger}), the REST {@code GET /api/version} endpoint, and the MCP {@code version}
|
* ({@link VersionLogger}) and the REST {@code GET /api/version} endpoint (both reference
|
||||||
* tool/server-info (all reference {@code agenticcode.version} rather than duplicating it).
|
* {@code agenticcode.version} rather than duplicating it).
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
public record VersionInfo(String name, String version) {
|
public record VersionInfo(String name, String version) {
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
# HTTP server port
|
# HTTP server port
|
||||||
quarkus.http.port=8787
|
quarkus.http.port=8787
|
||||||
# AgenticCode's own release counter (not the Maven project version) — bump this by hand for each
|
# AgenticCode's own release counter (not the Maven project version) — bump this by hand for each
|
||||||
# release. Single source of truth for the startup log line, GET /api/version, and the MCP
|
# release. Single source of truth for the startup log line, GET /api/version, and the OpenAPI
|
||||||
# 'version' tool/server-info (referenced below via property expression, not duplicated).
|
# info version (referenced below via property expression, not duplicated).
|
||||||
agenticcode.version=141
|
agenticcode.version=151
|
||||||
|
|
||||||
# MCP server (HTTP/SSE transport) — tools exposed at http://<host>:8787/mcp/sse
|
|
||||||
quarkus.mcp.server.server-info.name=agenticcode
|
|
||||||
quarkus.mcp.server.server-info.version=${agenticcode.version}
|
|
||||||
# OpenAPI / Swagger UI (item 48) — the generated spec is the contract the web-UI TS client
|
# OpenAPI / Swagger UI (item 48) — the generated spec is the contract the web-UI TS client
|
||||||
# is generated against. Served at /q/openapi (yaml/json); Swagger UI at /q/swagger-ui in dev.
|
# is generated against. Served at /q/openapi (yaml/json); Swagger UI at /q/swagger-ui in dev.
|
||||||
mp.openapi.extensions.smallrye.info.title=AgenticCode API
|
mp.openapi.extensions.smallrye.info.title=AgenticCode API
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
package com.agenticcode.codeserver.mcp;
|
|
||||||
|
|
||||||
import com.agenticcode.codeserver.api.ProjectResource;
|
|
||||||
import io.quarkiverse.mcp.server.TextContent;
|
|
||||||
import io.quarkiverse.mcp.server.test.McpAssured;
|
|
||||||
import io.quarkiverse.mcp.server.test.McpAssured.McpSseTestClient;
|
|
||||||
import io.quarkus.test.junit.QuarkusTest;
|
|
||||||
import io.restassured.RestAssured;
|
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static io.restassured.RestAssured.given;
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* End-to-end test of the MCP HTTP/SSE endpoint: connects an MCP client, asserts the tools are
|
|
||||||
* registered, then drives the full pipeline through tools only — {@code refresh} scans the
|
|
||||||
* project root and persists to Neo4j, and the query tools read it back — mirroring what an agent
|
|
||||||
* does. The project itself is created via REST (project CRUD is deliberately not an MCP tool).
|
|
||||||
*/
|
|
||||||
@QuarkusTest
|
|
||||||
class McpToolsIT {
|
|
||||||
|
|
||||||
private static final String PROJECT = "mcp-test-project";
|
|
||||||
|
|
||||||
@TempDir
|
|
||||||
static Path root;
|
|
||||||
|
|
||||||
private static int testPort;
|
|
||||||
|
|
||||||
@BeforeAll
|
|
||||||
static void setUp() {
|
|
||||||
testPort = Integer.getInteger("quarkus.http.test-port", 8081);
|
|
||||||
RestAssured.port = testPort;
|
|
||||||
copyFixture("fixtures/natural/YADDRBN0_SAMPLE.nat");
|
|
||||||
copyFixture("fixtures/natural/VDB2_VERSIS_ADDRESS.pda");
|
|
||||||
given()
|
|
||||||
.contentType("application/json")
|
|
||||||
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natural", null, null))
|
|
||||||
.when().post("/api/projects/" + PROJECT)
|
|
||||||
.then().statusCode(201);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static McpSseTestClient connect() {
|
|
||||||
return McpAssured.newSseClient()
|
|
||||||
.setBaseUri(URI.create("http://localhost:" + testPort))
|
|
||||||
.build()
|
|
||||||
.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String text(io.quarkiverse.mcp.server.ToolResponse response) {
|
|
||||||
return ((TextContent) response.firstContent()).text();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void copyFixture(String classpathResource) {
|
|
||||||
String fileName = classpathResource.substring(classpathResource.lastIndexOf('/') + 1);
|
|
||||||
try (InputStream in = McpToolsIT.class.getClassLoader().getResourceAsStream(classpathResource)) {
|
|
||||||
if (in == null) {
|
|
||||||
throw new IllegalStateException("Resource not found: " + classpathResource);
|
|
||||||
}
|
|
||||||
Files.write(root.resolve(fileName), in.readAllBytes());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new UncheckedIOException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void toolsAreRegistered() {
|
|
||||||
McpSseTestClient client = connect();
|
|
||||||
client.when()
|
|
||||||
.toolsList(page -> {
|
|
||||||
for (String tool : List.of("version", "list_projects", "list_modules", "callers", "callees",
|
|
||||||
"call_tree", "db_accesses", "search_identifier", "refresh")) {
|
|
||||||
assertNotNull(page.findByName(tool), "tool not registered: " + tool);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.thenAssertResults();
|
|
||||||
client.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void versionToolReturnsNameAndVersion() {
|
|
||||||
McpSseTestClient client = connect();
|
|
||||||
client.when()
|
|
||||||
.toolsCall("version", Map.of(), response -> {
|
|
||||||
assertFalse(response.isError(), text(response));
|
|
||||||
assertTrue(text(response).contains("\"name\":\"agenticcode\""), text(response));
|
|
||||||
})
|
|
||||||
.thenAssertResults();
|
|
||||||
client.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void ingestAndQueryThroughTools() {
|
|
||||||
McpSseTestClient client = connect();
|
|
||||||
|
|
||||||
// Ingest the whole project root via the MCP refresh tool.
|
|
||||||
client.when()
|
|
||||||
.toolsCall("refresh", Map.of("project", PROJECT, "deep", true), response -> {
|
|
||||||
assertFalse(response.isError(), "refresh returned an error: " + text(response));
|
|
||||||
assertTrue(text(response).contains("\"ingested\""), text(response));
|
|
||||||
})
|
|
||||||
.thenAssertResults();
|
|
||||||
|
|
||||||
// The ingested module is now listed.
|
|
||||||
client.when()
|
|
||||||
.toolsCall("list_modules", Map.of("project", PROJECT), response -> {
|
|
||||||
assertFalse(response.isError(), text(response));
|
|
||||||
assertTrue(text(response).contains("YADDRBN0_SAMPLE.nat"), text(response));
|
|
||||||
})
|
|
||||||
.thenAssertResults();
|
|
||||||
|
|
||||||
// list_projects surfaces the project we created.
|
|
||||||
client.when()
|
|
||||||
.toolsCall("list_projects", Map.of(), response -> {
|
|
||||||
assertFalse(response.isError(), text(response));
|
|
||||||
assertTrue(text(response).contains(PROJECT), text(response));
|
|
||||||
})
|
|
||||||
.thenAssertResults();
|
|
||||||
|
|
||||||
client.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void unknownProjectIsAToolError() {
|
|
||||||
McpSseTestClient client = connect();
|
|
||||||
client.when()
|
|
||||||
.toolsCall("search_identifier", Map.of("project", "does-not-exist", "name", "X"), response -> {
|
|
||||||
assertTrue(response.isError(), "expected a tool error for a missing project");
|
|
||||||
assertTrue(text(response).contains("PROJECT_NOT_FOUND"), text(response));
|
|
||||||
})
|
|
||||||
.thenAssertResults();
|
|
||||||
client.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
pom.xml
13
pom.xml
@@ -30,7 +30,6 @@
|
|||||||
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||||
<quarkus.platform.version>3.36.2</quarkus.platform.version>
|
<quarkus.platform.version>3.36.2</quarkus.platform.version>
|
||||||
<quarkus-neo4j.version>6.6.1</quarkus-neo4j.version>
|
<quarkus-neo4j.version>6.6.1</quarkus-neo4j.version>
|
||||||
<mcp-server.version>1.9.1</mcp-server.version>
|
|
||||||
<javaparser.version>3.26.2</javaparser.version>
|
<javaparser.version>3.26.2</javaparser.version>
|
||||||
<junit.version>5.11.3</junit.version>
|
<junit.version>5.11.3</junit.version>
|
||||||
<compiler-plugin.version>3.13.0</compiler-plugin.version>
|
<compiler-plugin.version>3.13.0</compiler-plugin.version>
|
||||||
@@ -93,18 +92,6 @@
|
|||||||
<version>${quarkus-neo4j.version}</version>
|
<version>${quarkus-neo4j.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkiverse.mcp</groupId>
|
|
||||||
<artifactId>quarkus-mcp-server-sse</artifactId>
|
|
||||||
<version>${mcp-server.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkiverse.mcp</groupId>
|
|
||||||
<artifactId>quarkus-mcp-server-test</artifactId>
|
|
||||||
<version>${mcp-server.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
|||||||
@@ -72,10 +72,9 @@ endpoint, its parameters, its response shape, and the semantics behind them. It
|
|||||||
before using the API in anger, and consult it whenever a response does not look the way you expected. What
|
before using the API in anger, and consult it whenever a response does not look the way you expected. What
|
||||||
follows here is only the short orientation for this project's two jobs, not a replacement.
|
follows here is only the short orientation for this project's two jobs, not a replacement.
|
||||||
|
|
||||||
**Priority: MCP tools → REST API → `ac` CLI → grep/Explore.** Use `mcp__agenticcode__*` first. If an MCP
|
**Priority: REST API → `ac` CLI → grep/Explore.** Query the REST endpoints
|
||||||
call fails with a session/protocol error, fall back to the REST endpoint
|
(`GET /api/projects/{project}/modules/{name}/...`) first; the `ac` CLI wraps the same API for quick
|
||||||
(`GET /api/projects/{project}/modules/{name}/...`) — do not let one broken MCP call push you to grep.
|
manual checks. Fall back to reading source directly only when the question needs exact text, comments, or formatting
|
||||||
Fall back to reading source directly only when the question needs exact text, comments, or formatting
|
|
||||||
(which it often does here — see §3).
|
(which it often does here — see §3).
|
||||||
|
|
||||||
Endpoints that matter for this work:
|
Endpoints that matter for this work:
|
||||||
@@ -87,9 +86,9 @@ Endpoints that matter for this work:
|
|||||||
| Full reachable closure | `call-tree?depth=N` |
|
| Full reachable closure | `call-tree?depth=N` |
|
||||||
| Which DB tables, read or written? | `db-accesses?depth=N` |
|
| Which DB tables, read or written? | `db-accesses?depth=N` |
|
||||||
| Actual SQL text | `sql-statements?depth=N` |
|
| Actual SQL text | `sql-statements?depth=N` |
|
||||||
| Subroutines / methods | `module_functions`, `module_context` |
|
| Subroutines / methods | `functions`, `context` |
|
||||||
| Work files | `workfile_accesses` |
|
| Work files | `workfile-accesses` |
|
||||||
| Find a constant/string | `search_value`, `search_identifier` |
|
| Find a constant/string | `search/value`, `search/identifier` |
|
||||||
|
|
||||||
### Known API pitfalls (verified — do not re-discover these)
|
### Known API pitfalls (verified — do not re-discover these)
|
||||||
|
|
||||||
@@ -139,7 +138,7 @@ public class MultiTableImportJob extends AbstractPurBatchJob {
|
|||||||
|
|
||||||
To go **Java → Natural**: read `PROGRAM_IDENTIFIER` / the `// XXXXXXXX.nat` comment, then confirm the module
|
To go **Java → Natural**: read `PROGRAM_IDENTIFIER` / the `// XXXXXXXX.nat` comment, then confirm the module
|
||||||
exists in `upms`.
|
exists in `upms`.
|
||||||
To go **Natural → Java**: `search_value` the program name in `pur`. **An empty result means the program has
|
To go **Natural → Java**: `search/value` the program name in `pur`. **An empty result means the program has
|
||||||
not been reengineered yet** — that is the normal starting state for a reengineering task, not an error.
|
not been reengineered yet** — that is the normal starting state for a reengineering task, not an error.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -150,8 +149,8 @@ not been reengineered yet** — that is the normal starting state for a reengine
|
|||||||
|
|
||||||
1. **Establish the pair** (§3) and state it explicitly.
|
1. **Establish the pair** (§3) and state it explicitly.
|
||||||
2. **Build the Natural ground truth via the API**: `call-tree` for the full module closure,
|
2. **Build the Natural ground truth via the API**: `call-tree` for the full module closure,
|
||||||
`db-accesses?depth=N` for the real tables (note the `via` chain), `sql-statements`, `module_functions`
|
`db-accesses?depth=N` for the real tables (note the `via` chain), `sql-statements`, `functions`
|
||||||
for the subroutine inventory, `workfile_accesses` for the I/O.
|
for the subroutine inventory, `workfile-accesses` for the I/O.
|
||||||
3. **Read the whole Natural source.** The API gives you structure; the semantics live in the statements,
|
3. **Read the whole Natural source.** The API gives you structure; the semantics live in the statements,
|
||||||
and — critically — in the **comments and commented-out code**. Natural programs here carry decisive
|
and — critically — in the **comments and commented-out code**. Natural programs here carry decisive
|
||||||
information in comments: change history (`#01`…`#04` markers correlate to `--> #04` / `<-- #04` blocks in
|
information in comments: change history (`#01`…`#04` markers correlate to `--> #04` / `<-- #04` blocks in
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Perform a **very deep analysis of all agentic API endpoints**, in two tiers:
|
|||||||
## How to work
|
## How to work
|
||||||
|
|
||||||
1. **Use the REST API** (`GET /api/projects/pur/modules/{name}/...`), falling back to the `ac` CLI
|
1. **Use the REST API** (`GET /api/projects/pur/modules/{name}/...`), falling back to the `ac` CLI
|
||||||
for quick manual checks. Do not use MCP for this audit.
|
for quick manual checks.
|
||||||
For MultiTableImportJob (and any escalated module) pull every relevant endpoint: `callees`,
|
For MultiTableImportJob (and any escalated module) pull every relevant endpoint: `callees`,
|
||||||
`callers`, `db-accesses`, `functions`, `data-structures`, `dispatch-table`, `digest`, `context`,
|
`callers`, `db-accesses`, `functions`, `data-structures`, `dispatch-table`, `digest`, `context`,
|
||||||
`call-tree`, `sql-statements`, `graph`.
|
`call-tree`, `sql-statements`, `graph`.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Perform a **very deep analysis of all agentic API endpoints**, in two tiers:
|
|||||||
IMPORTANT: use the API described in `x-docs/agent-api-system-prompt.md` to analyze WGEAGB0S
|
IMPORTANT: use the API described in `x-docs/agent-api-system-prompt.md` to analyze WGEAGB0S
|
||||||
|
|
||||||
1. **Use the REST API** (`GET /api/projects/upms/modules/{name}/...`), falling back to the `ac` CLI
|
1. **Use the REST API** (`GET /api/projects/upms/modules/{name}/...`), falling back to the `ac` CLI
|
||||||
for quick manual checks. Do not use MCP for this audit.
|
for quick manual checks.
|
||||||
For WGEAGB0S (and any escalated module) pull every relevant endpoint: `callees`, `callers`,
|
For WGEAGB0S (and any escalated module) pull every relevant endpoint: `callees`, `callers`,
|
||||||
`db-accesses`, `functions`, `data-structures`, `dispatch-table`, `digest`, `context`,
|
`db-accesses`, `functions`, `data-structures`, `dispatch-table`, `digest`, `context`,
|
||||||
`call-tree`, `sql-statements`, `graph`.
|
`call-tree`, `sql-statements`, `graph`.
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# AgenticCode API — Agent System Prompt
|
# AgenticCode API — Agent System Prompt
|
||||||
|
|
||||||
You are an agent that analyzes source code (Software AG **Natural** and
|
You are an agent that analyzes source code (Software AG **Natural** and
|
||||||
**Java**) through the AgenticCode REST API (or its MCP-tool equivalents — see
|
**Java**) through the AgenticCode REST API. The server has already parsed the source into a unified AST
|
||||||
"Access via MCP"). The server has already parsed the source into a unified AST
|
|
||||||
stored as a graph in Neo4j; you query that graph read-only. Use the API as
|
stored as a graph in Neo4j; you query that graph read-only. Use the API as
|
||||||
your source of truth about the code structure — do not guess at structure you
|
your source of truth about the code structure — do not guess at structure you
|
||||||
can look up. (You may still need to read actual source text — see "Reading
|
can look up. (You may still need to read actual source text — see "Reading
|
||||||
@@ -450,69 +449,6 @@ curl http://localhost:8787/api/projects/demo/nodes/3f9c1a2b-.../source
|
|||||||
curl 'http://localhost:8787/api/projects/demo/modules/ZSNNA12/source?startLine=10&endLine=25'
|
curl 'http://localhost:8787/api/projects/demo/modules/ZSNNA12/source?startLine=10&endLine=25'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Access via MCP
|
|
||||||
|
|
||||||
Same operations, exposed as MCP tools over HTTP/SSE at
|
|
||||||
`http://<host>:8787/mcp/sse` (server name `agenticcode`):
|
|
||||||
|
|
||||||
```
|
|
||||||
claude mcp add --transport sse agenticcode http://localhost:8787/mcp/sse
|
|
||||||
```
|
|
||||||
|
|
||||||
Each tool takes the same arguments as its REST twin (`{project}` path segment
|
|
||||||
|
|
||||||
+ query params become named arguments) and returns identical JSON. Failures
|
|
||||||
come back as `isError=true` with the same `{ error, code, details }` body.
|
|
||||||
`flow_forward`/`flow_backward`/`field_flow` return an actionable hint
|
|
||||||
pointing at `ingest_module` when the module isn't deep-ingested — out of
|
|
||||||
your read-only scope, same as the REST `409`.
|
|
||||||
|
|
||||||
**If MCP is unavailable** (not registered, session error, failed call): use
|
|
||||||
the equivalent REST endpoint instead of giving up — every tool below has one.
|
|
||||||
|
|
||||||
| REST endpoint | MCP tool |
|
|
||||||
|---------------------------------------------------------------------|---------------------------------|
|
|
||||||
| `GET /api/projects` | `list_projects` |
|
|
||||||
| `GET /api/version` | `version` |
|
|
||||||
| `GET /modules` | `list_modules` |
|
|
||||||
| `GET /modules/{name}/digest` | `module_digest` |
|
|
||||||
| `GET /modules/{name}/context` | `module_context` |
|
|
||||||
| `GET /modules/{name}/functions` | `module_functions` |
|
|
||||||
| `GET /modules/{name}/functions/{fn}/overrides` (or bulk, no `{fn}`) | `function_overrides` |
|
|
||||||
| `GET /modules/{name}/data-structures` | `module_data_structures` |
|
|
||||||
| `GET /modules/{name}/dispatch-table` | `module_dispatch_table` |
|
|
||||||
| `GET /modules/{name}/columns` | `module_columns` |
|
|
||||||
| `GET /modules/{name}/callers` | `callers` |
|
|
||||||
| `GET /modules/{name}/callees` | `callees` |
|
|
||||||
| `GET /modules/{name}/call-tree` | `call_tree` |
|
|
||||||
| `GET /modules/{name}/db-accesses` | `db_accesses` |
|
|
||||||
| `GET /modules/{name}/sql-statements` | `sql_statements` |
|
|
||||||
| `GET /data-structures/{name}/fields` | `data_structure_fields` |
|
|
||||||
| `GET /db-tables/{name}/columns` | `db_table_columns` |
|
|
||||||
| `GET /search/identifier` | `search_identifier` |
|
|
||||||
| `GET /search/value` | `search_value` |
|
|
||||||
| `GET /search/annotation` | `search_annotation` |
|
|
||||||
| `GET /nodes/{id}` | `inspect_node` |
|
|
||||||
| `GET /nodes/{id}/source` | `node_source` |
|
|
||||||
| `GET /modules/{name}/source` | `module_source` |
|
|
||||||
| `GET /variables/{name}/reads` | `variable_reads` |
|
|
||||||
| `GET /variables/{name}/writes` | `variable_writes` |
|
|
||||||
| `GET /variables/{name}/flow-forward` | `flow_forward` |
|
|
||||||
| `GET /variables/{name}/flow-backward` | `flow_backward` |
|
|
||||||
| `GET /variables/{name}/field-flow` | `field_flow` |
|
|
||||||
| `GET /dynamic-calls/unresolved` | `list_unresolved_dynamic_calls` |
|
|
||||||
| `GET /dynamic-calls/overrides` | `list_dynamic_call_overrides` |
|
|
||||||
| `POST /dynamic-calls/overrides` | `set_dynamic_call_override` |
|
|
||||||
| `DELETE /dynamic-calls/overrides` | `reset_dynamic_call_override` |
|
|
||||||
|
|
||||||
`set_dynamic_call_override` takes `targets` as a comma-separated string
|
|
||||||
(e.g. `"A,B"`); the REST body takes a JSON array. Both are your one write
|
|
||||||
capability (item 82) — see "Resolving unresolved dynamic `CALLNAT` calls".
|
|
||||||
Project create/update/delete and the global clear have no MCP tool (REST-only,
|
|
||||||
operator scope). Ingest tools exist but are out of scope for you, same as the
|
|
||||||
REST ingest endpoints. `?fields=name` is REST-only; MCP tools always return
|
|
||||||
the full shape.
|
|
||||||
|
|
||||||
## Recommended end-to-end flow
|
## Recommended end-to-end flow
|
||||||
|
|
||||||
1. `GET /api/projects` → pick the project.
|
1. `GET /api/projects` → pick the project.
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
Technische White-Box-Übersicht. AgenticCode ist ein Server, der Quellcode —
|
Technische White-Box-Übersicht. AgenticCode ist ein Server, der Quellcode —
|
||||||
**Software AG Natural** und **Java** — in einen einheitlichen Graphen zerlegt, in
|
**Software AG Natural** und **Java** — in einen einheitlichen Graphen zerlegt, in
|
||||||
Neo4j ablegt, semantisch anreichert und über eine agenten-optimierte Schnittstelle
|
Neo4j ablegt, semantisch anreichert und über eine agenten-optimierte Schnittstelle
|
||||||
(REST + MCP + CLI) abfragbar macht.
|
(REST + CLI) abfragbar macht.
|
||||||
|
|
||||||
> Grundlage: statische Analyse des Repositorys `ac` mit AgenticCode selbst
|
> Grundlage: statische Analyse des Repositorys `ac` mit AgenticCode selbst
|
||||||
> (Dogfooding). Stand: Juli 2026 · Server-Version 8. Die Beschreibung folgt dem
|
> (Dogfooding). Stand: Juli 2026 · Server-Version 8. Die Beschreibung folgt dem
|
||||||
> tatsächlichen Code, nicht einer Spezifikation.
|
> tatsächlichen Code, nicht einer Spezifikation.
|
||||||
|
|
||||||
**Tech-Stack:** Quarkus · Java 21 (Virtual Threads) · Neo4j 5 · Mutiny (reactive) ·
|
**Tech-Stack:** Quarkus · Java 21 (Virtual Threads) · Neo4j 5 · Mutiny (reactive) ·
|
||||||
NullAway · JavaParser + eigener Natural-Parser · MCP über HTTP/SSE.
|
NullAway · JavaParser + eigener Natural-Parser.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ NullAway · JavaParser + eigener Natural-Parser · MCP über HTTP/SSE.
|
|||||||
|
|
||||||
Der Kern ist eine Pipeline: jede Quelldatei wird geparst, in ein sprachunabhängiges
|
Der Kern ist eine Pipeline: jede Quelldatei wird geparst, in ein sprachunabhängiges
|
||||||
AST-Modell überführt, als Knoten-/Kanten-Graph in Neo4j persistiert, durch mehrere
|
AST-Modell überführt, als Knoten-/Kanten-Graph in Neo4j persistiert, durch mehrere
|
||||||
Anreicherungs-Schritte (Enrichment) verknüpft und über REST, MCP und CLI verfügbar
|
Anreicherungs-Schritte (Enrichment) verknüpft und über REST und CLI verfügbar
|
||||||
gemacht.
|
gemacht.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -33,7 +33,7 @@ Quelldateien → Parser (Natural/Java) → Unified AST → Neo4j-Graph → Enric
|
|||||||
| `ac-parser-natural` | Eigener Parser für Software AG Natural — es gibt keinen brauchbaren OSS-Parser. Zeilen-/Regex-basiert. Plus lexer-basierter Grob-Scanner (Tier 1). |
|
| `ac-parser-natural` | Eigener Parser für Software AG Natural — es gibt keinen brauchbaren OSS-Parser. Zeilen-/Regex-basiert. Plus lexer-basierter Grob-Scanner (Tier 1). |
|
||||||
| `ac-parser-java` | Java über die `JavaParser`-Bibliothek (mit Symbol-Auflösung). Grob-Scan als Projektion des vollen Parses. |
|
| `ac-parser-java` | Java über die `JavaParser`-Bibliothek (mit Symbol-Auflösung). Grob-Scan als Projektion des vollen Parses. |
|
||||||
| `ac-neo4j-store` | Persistenz & alle Cypher-Abfragen (`GraphRepository`, `CypherQueries`), Enrichment-Schritte. |
|
| `ac-neo4j-store` | Persistenz & alle Cypher-Abfragen (`GraphRepository`, `CypherQueries`), Enrichment-Schritte. |
|
||||||
| `ac-code-server` | Quarkus-App: REST-Ressourcen, MCP-Tools, Dienste (Ingest, Deep-Ingest-Koordinator, Source-Snippets). |
|
| `ac-code-server` | Quarkus-App: REST-Ressourcen, Dienste (Ingest, Deep-Ingest-Koordinator, Source-Snippets). |
|
||||||
| `ac-cli` | Kommandozeilen-Client (picocli) — dünner Wrapper über die REST-API. |
|
| `ac-cli` | Kommandozeilen-Client (picocli) — dünner Wrapper über die REST-API. |
|
||||||
|
|
||||||
### Das einheitliche AST-Modell
|
### Das einheitliche AST-Modell
|
||||||
@@ -99,29 +99,27 @@ mitgeliefert.
|
|||||||
|
|
||||||
## 2. Verwendung: CLI, API, Agenten
|
## 2. Verwendung: CLI, API, Agenten
|
||||||
|
|
||||||
Es gibt drei gleichwertige Zugänge auf dieselbe Logik. Für KI-Agenten ist die empfohlene
|
Es gibt zwei gleichwertige Zugänge auf dieselbe Logik. Für KI-Agenten ist die empfohlene
|
||||||
Reihenfolge: **MCP zuerst**, dann REST, dann CLI, und erst als letztes klassisches
|
Reihenfolge: **REST zuerst**, dann CLI, und erst als letztes klassisches
|
||||||
Durchsuchen (grep).
|
Durchsuchen (grep).
|
||||||
|
|
||||||
- **MCP (für Agenten):** Werkzeuge `mcp__agenticcode__*` über HTTP/SSE. Native
|
- **REST (programmatisch, auch für Agenten):** JAX-RS unter `/api/projects/{p}/…`. Kompaktes JSON,
|
||||||
Integration in Claude Code & andere Agenten.
|
|
||||||
- **REST (programmatisch):** JAX-RS unter `/api/projects/{p}/…`. Kompaktes JSON,
|
|
||||||
Pagination via `limit`/`offset`.
|
Pagination via `limit`/`offset`.
|
||||||
- **CLI (manuell/Skripte):** `ac <befehl>` — dünner Wrapper über REST, für schnelle
|
- **CLI (manuell/Skripte):** `ac <befehl>` — dünner Wrapper über REST, für schnelle
|
||||||
Checks am Terminal.
|
Checks am Terminal.
|
||||||
|
|
||||||
### Was man abfragen kann
|
### Was man abfragen kann
|
||||||
|
|
||||||
| Zweck | MCP-Tool / REST | CLI |
|
| Zweck | REST-Endpunkt | CLI |
|
||||||
|-------------------------|--------------------------------------------|------------------------|
|
|-------------------------|-----------------------------------------------|------------------------|
|
||||||
| Wer ruft auf? | `callers` · `/modules/{n}/callers` | `ac callers` |
|
| Wer ruft auf? | `/modules/{n}/callers` | `ac callers` |
|
||||||
| Was wird aufgerufen? | `callees` · `/modules/{n}/callees` | `ac callees` |
|
| Was wird aufgerufen? | `/modules/{n}/callees` | `ac callees` |
|
||||||
| Transitiver Aufrufbaum | `call_tree` · `/call-tree` | `ac call-tree` |
|
| Transitiver Aufrufbaum | `/modules/{n}/call-tree` | `ac call-tree` |
|
||||||
| Modul-Überblick | `module_context` · `/context` | `ac context` |
|
| Modul-Überblick | `/modules/{n}/context` | `ac context` |
|
||||||
| DB-Zugriffe | `db_accesses` · `/db-accesses` | `ac db-accesses` |
|
| DB-Zugriffe | `/modules/{n}/db-accesses` | `ac db-accesses` |
|
||||||
| Bezeichner suchen | `search_identifier` · `/search/identifier` | `ac search-identifier` |
|
| Bezeichner suchen | `/search/identifier` | `ac search-identifier` |
|
||||||
| Datenfluss verfolgen | `flow_forward` / `field_flow` | `ac flow-forward` |
|
| Datenfluss verfolgen | `/variables/{n}/flow-forward` · `/field-flow` | `ac flow-forward` |
|
||||||
| Re-Ingest nach Änderung | `refresh` · `POST /refresh` | `ac refresh` |
|
| Re-Ingest nach Änderung | `POST /refresh` | `ac refresh` |
|
||||||
|
|
||||||
### Typischer Ablauf
|
### Typischer Ablauf
|
||||||
|
|
||||||
@@ -201,11 +199,10 @@ Legende: **[Design]** = bewusste Näherung · **[Limit]** = echte Einschränkung
|
|||||||
|
|
||||||
- **[Limit] Node-IDs sind nicht stabil · `unresolved` nur punktuell.** Knoten-`id`s werden
|
- **[Limit] Node-IDs sind nicht stabil · `unresolved` nur punktuell.** Knoten-`id`s werden
|
||||||
bei jedem Re-Ingest neu vergeben — gecachte IDs veralten. Das `unresolved`-Flag wird
|
bei jedem Re-Ingest neu vergeben — gecachte IDs veralten. Das `unresolved`-Flag wird
|
||||||
explizit nur von `search_identifier` und `inspect_node` ausgegeben; in `callers`/`callees`
|
explizit nur von `/search/identifier` und `/nodes/{id}` ausgegeben; in `callers`/`callees`
|
||||||
ist es nur indirekt (leeres `sourceFile`) erkennbar.
|
ist es nur indirekt (leeres `sourceFile`) erkennbar.
|
||||||
|
|
||||||
- **[Umfang] Zwei Sprachen · offene Performance-Baustellen.** Unterstützt werden
|
- **[Umfang] Zwei Sprachen · offene Performance-Baustellen.** Unterstützt werden
|
||||||
ausschließlich **Natural** und **Java**. Ein tiefer Ganz-Projekt-Ingest ist auf großen
|
ausschließlich **Natural** und **Java**. Ein tiefer Ganz-Projekt-Ingest ist auf großen
|
||||||
Codebasen teuer (Enrichment im Minutenbereich) — genau deshalb das lazy Modell. Paralleles
|
Codebasen teuer (Enrichment im Minutenbereich) — genau deshalb das lazy Modell. Paralleles
|
||||||
Parsen und gebündeltes Persistieren sind noch offen; die MCP-SSE-Session zeigt gelegentlich
|
Parsen und gebündeltes Persistieren sind noch offen.
|
||||||
sporadische Reconnect-Fehler (vermutlich clientseitig).
|
|
||||||
|
|||||||
@@ -2217,3 +2217,33 @@ endpoint already exposed, so an unresolved dynamic target is machine-distinguish
|
|||||||
now **requires** an agent to investigate and pin an unresolved dynamic call rather than report a dead end.
|
now **requires** an agent to investigate and pin an unresolved dynamic call rather than report a dead end.
|
||||||
Covered by `DynamicCallOverrideIT` (6/0/0): resolve, multi-target, unknown-target rejection, inline reset
|
Covered by `DynamicCallOverrideIT` (6/0/0): resolve, multi-target, unknown-target rejection, inline reset
|
||||||
restore, obsolete precedence, and override survival across a `refresh?deep=true`.
|
restore, obsolete precedence, and override survival across a `refresh?deep=true`.
|
||||||
|
|
||||||
|
## MCP surface removed (item 26) — 2026-08-04
|
||||||
|
|
||||||
|
The MCP server was removed from the product. It existed as a second transport in front of the same
|
||||||
|
services the REST API already exposes: `McpQueryTools` (770 LoC, 38 `@Tool` methods), `McpIngestTools`
|
||||||
|
(2 tools), plus `McpSupport`/`McpLogged`/`McpLoggingInterceptor` — 973 LoC of main code and the
|
||||||
|
145-LoC `McpToolsIT`, all deleted, together with the `quarkus-mcp-server-sse`/`-test` dependencies
|
||||||
|
(root POM `dependencyManagement` + `mcp-server.version` property + `ac-code-server` POM), the
|
||||||
|
`quarkus.mcp.server.server-info.*` properties, and the repo-root `.mcp.json` client registration.
|
||||||
|
|
||||||
|
**Rationale.** Item 26 (MCP session reliability) never became fixable from this codebase: calls failed
|
||||||
|
with `"the first message from the client must be initialize: tools/call"`, at first intermittently and
|
||||||
|
by 2026-08-02 from the first call of a session onwards, while the equivalent REST endpoints answered
|
||||||
|
normally. Rather than carry a second, unreliable transport plus the CLAUDE.md rule that every REST
|
||||||
|
change be mirrored into an MCP tool, the surface was dropped.
|
||||||
|
|
||||||
|
**No capability lost.** All 40 tools were verified to have a REST twin before deletion — including the
|
||||||
|
non-obvious ones: `version` → `GET /api/version`, `ego_graph` → `GET /modules/{name}/graph`,
|
||||||
|
`file_source` → `GET /source?file=`, `project_loc` → `GET /loc`, and the three dynamic-call-override
|
||||||
|
tools → `GET`/`POST`/`DELETE /api/projects/{p}/dynamic-calls/overrides`. The MCP layer held no logic of
|
||||||
|
its own; `McpSupport` only serialized service results into the same JSON the REST resources return.
|
||||||
|
|
||||||
|
**Docs & rules updated.** `CLAUDE.md` (sync rule now REST + `ac-cli`; tool priority now REST → CLI →
|
||||||
|
grep/Explore; architecture diagram, project structure, ADR table), `README.md` (architecture diagram,
|
||||||
|
tech stack, endpoint list, "Agent usage" section replacing "MCP"), `x-docs/agent-api-system-prompt.md`
|
||||||
|
("Access via MCP" section and its REST↔tool mapping table dropped), `x-docs/mcp-api-usage-ac-implementation.md`
|
||||||
|
(tool names replaced by their endpoint paths throughout; filename kept to avoid breaking ~6 inbound
|
||||||
|
references), `x-docs/agenticcode-ueberblick.md`, `x-docs/presentation.md`, `prompts/CLAUDE.md`, both
|
||||||
|
`prompts/*-deep-api-audit.md`, and `.claude/settings.local.json`. Historical MCP mentions in this file
|
||||||
|
and in earlier `roadmap.md` entries are left intact as record.
|
||||||
|
|||||||
@@ -6,19 +6,18 @@ call graphs, callers/callees, DB access, dataflow, and module overviews when
|
|||||||
working *on this repo* — it's exactly the tool this project builds.
|
working *on this repo* — it's exactly the tool this project builds.
|
||||||
|
|
||||||
**For full API semantics (params, response shapes, error codes, language
|
**For full API semantics (params, response shapes, error codes, language
|
||||||
applicability, MCP tool mapping, curl examples)**, see
|
applicability, curl examples)**, see
|
||||||
`x-docs/agent-api-system-prompt.md` — that file is the canonical reference and
|
`x-docs/agent-api-system-prompt.md` — that file is the canonical reference and
|
||||||
is not duplicated here. This file only covers what's specific to using the
|
is not duplicated here. This file only covers what's specific to using the
|
||||||
API *as Claude Code, on this checkout*.
|
API *as Claude Code, on this checkout*.
|
||||||
|
|
||||||
## Tool priority
|
## Tool priority
|
||||||
|
|
||||||
MCP first (`mcp__agenticcode__*`) → REST (`GET /api/projects/ac/...`) → `ac`
|
REST first (`GET /api/projects/ac/...`) → `ac`
|
||||||
CLI (`ac callers`, `ac callees`, `ac call-tree`, `ac context`, `ac
|
CLI (`ac callers`, `ac callees`, `ac call-tree`, `ac context`, `ac
|
||||||
db-accesses`, ...) → grep/Explore. Only fall back past REST/CLI when the
|
db-accesses`, ...) → grep/Explore. Only fall back past REST/CLI when the
|
||||||
question genuinely isn't answerable by this API at all (see "Missing
|
question genuinely isn't answerable by this API at all (see "Missing
|
||||||
capability" below). If an MCP call fails (session/protocol error), fall back
|
capability" below). If the server is unreachable, try
|
||||||
to REST rather than abandoning the API. If the server is unreachable, try
|
|
||||||
`./manage-ac.sh deploy` before falling back further.
|
`./manage-ac.sh deploy` before falling back further.
|
||||||
|
|
||||||
## Re-ingest before trusting results
|
## Re-ingest before trusting results
|
||||||
@@ -27,16 +26,15 @@ Query results reflect the last ingest, not the current working tree.
|
|||||||
**Refresh after code changes** before trusting query results:
|
**Refresh after code changes** before trusting query results:
|
||||||
`ac refresh` or `POST /api/projects/ac/refresh` (add `--deep` / `?deep=true` for a
|
`ac refresh` or `POST /api/projects/ac/refresh` (add `--deep` / `?deep=true` for a
|
||||||
full field-level pass). `refresh` is the single (re-)ingest surface (item 42) — the
|
full field-level pass). `refresh` is the single (re-)ingest surface (item 42) — the
|
||||||
eager `ingest_all`/`ingest_module`/`ingest-call-graph` tools were removed.
|
eager `ingest-all`/`ingest-module`/`ingest-call-graph` endpoints were removed.
|
||||||
`ac refresh <name>` deep-ingests one module + its callees/data areas; add
|
`ac refresh <name>` deep-ingests one module + its callees/data areas; add
|
||||||
`--neighborhood` (`POST /refresh/{name}?scope=neighborhood`) to also pull in the
|
`--neighborhood` (`POST /refresh/{name}?scope=neighborhood`) to also pull in the
|
||||||
module's transitive **callers** (whole call-graph neighbourhood). `refresh` is
|
module's transitive **callers** (whole call-graph neighbourhood).
|
||||||
REST + CLI only — mutations aren't exposed as MCP tools.
|
|
||||||
|
|
||||||
**Reconciliation on re-ingest (item 58).** A `refresh` now **purges stale nodes**:
|
**Reconciliation on re-ingest (item 58).** A `refresh` now **purges stale nodes**:
|
||||||
for every re-parsed file it deletes the nodes the fresh parse no longer produces
|
for every re-parsed file it deletes the nodes the fresh parse no longer produces
|
||||||
(renamed/removed fields, moved statements) rather than leaving them to shadow the
|
(renamed/removed fields, moved statements) rather than leaving them to shadow the
|
||||||
new ones — so identifier counts and `search_identifier` results stay clean after a
|
new ones — so identifier counts and `/search/identifier` results stay clean after a
|
||||||
parser change or an edited source file. Applies to every full-parse path (whole-root
|
parser change or an edited source file. Applies to every full-parse path (whole-root
|
||||||
`refresh` with or without `--deep`, and per-module `refresh/{name}`); the coarse
|
`refresh` with or without `--deep`, and per-module `refresh/{name}`); the coarse
|
||||||
Tier-1 scan run at project creation does not reconcile (it only ever runs on an empty
|
Tier-1 scan run at project creation does not reconcile (it only ever runs on an empty
|
||||||
@@ -66,8 +64,7 @@ agents with no filesystem access.
|
|||||||
|
|
||||||
**Stale-source check (item 41).** The `/source` endpoints compare the file on
|
**Stale-source check (item 41).** The `/source` endpoints compare the file on
|
||||||
disk against the content hash (`sourceHash`) stored at ingest. If the file
|
disk against the content hash (`sourceHash`) stored at ingest. If the file
|
||||||
changed since the last ingest they return `409 STALE_SOURCE` (MCP: a
|
changed since the last ingest they return `409 STALE_SOURCE` instead of slicing current text against old line
|
||||||
`STALE_SOURCE` tool error) instead of slicing current text against old line
|
|
||||||
numbers — re-ingest (refresh) the project to update the graph. Line ranges you
|
numbers — re-ingest (refresh) the project to update the graph. Line ranges you
|
||||||
read directly off disk are of course always current; this only guards the API's
|
read directly off disk are of course always current; this only guards the API's
|
||||||
own slicing. Copycode/INCLUDE slices are raw pre-expansion file text.
|
own slicing. Copycode/INCLUDE slices are raw pre-expansion file text.
|
||||||
@@ -92,8 +89,8 @@ A reference whose target isn't (yet) ingested — a `CALLNAT`/`PERFORM`/`USING`
|
|||||||
absent from the project, or a dynamic `CALLNAT PGM-VAR` whose literal can't be recovered — is stored
|
absent from the project, or a dynamic `CALLNAT PGM-VAR` whose literal can't be recovered — is stored
|
||||||
as a **deduped placeholder node** (blank `sourceFile`). Enrichment stamps each with an `unresolved`
|
as a **deduped placeholder node** (blank `sourceFile`). Enrichment stamps each with an `unresolved`
|
||||||
boolean: `true` while genuinely dangling, `false` once a real definition of that name is ingested.
|
boolean: `true` while genuinely dangling, `false` once a real definition of that name is ingested.
|
||||||
`search_identifier` returns it as `unresolved` on each `IdentifierMatch`, and `inspect_node`
|
`/search/identifier` returns it as `unresolved` on each `IdentifierMatch`, and `GET /nodes/{id}` carries it in the
|
||||||
(`GET /nodes/{id}`) carries it in the node's properties — so an agent can tell a dangling/dynamic
|
node's properties — so an agent can tell a dangling/dynamic
|
||||||
reference apart from a resolved one. In `callers`/`callees` such targets already appear as entries
|
reference apart from a resolved one. In `callers`/`callees` such targets already appear as entries
|
||||||
with a blank `sourceFile`.
|
with a blank `sourceFile`.
|
||||||
|
|
||||||
@@ -199,10 +196,10 @@ reproducible project totals.
|
|||||||
|
|
||||||
Where to read them:
|
Where to read them:
|
||||||
|
|
||||||
- `list_modules` (`GET /modules`) — `loc`/`sloc` on each row.
|
- `GET /modules` — `loc`/`sloc` on each row.
|
||||||
- `module_context` (`GET /modules/{name}/context`) — `loc`/`sloc` on the module.
|
- `GET /modules/{name}/context` — `loc`/`sloc` on the module.
|
||||||
- `inspect_node` (`GET /nodes/{id}`) — `loc`/`sloc` in the node's raw properties.
|
- `GET /nodes/{id}` — `loc`/`sloc` in the node's raw properties.
|
||||||
- `project_loc` (`GET /loc`, `ac loc`) — the rollup: a per-language breakdown (`fileCount`, `loc`,
|
- `GET /loc` (`ac loc`) — the rollup: a per-language breakdown (`fileCount`, `loc`,
|
||||||
`sloc`) plus a project-wide total, optionally narrowed by `?language=` / `?sourceFile=`. Each source
|
`sloc`) plus a project-wide total, optionally narrowed by `?language=` / `?sourceFile=`. Each source
|
||||||
file is counted once even when it yields several nodes (Java inner classes, Natural inline groups).
|
file is counted once even when it yields several nodes (Java inner classes, Natural inline groups).
|
||||||
|
|
||||||
@@ -225,7 +222,7 @@ below; it never contributes nodes/edges. So when verifying an API response again
|
|||||||
module, always read the `generatedDir` file (e.g. `generated_src/subprogram/WGEAGB0S.nat`), not the
|
module, always read the `generatedDir` file (e.g. `generated_src/subprogram/WGEAGB0S.nat`), not the
|
||||||
`user_exit` fragment.
|
`user_exit` fragment.
|
||||||
|
|
||||||
`project_loc` (`GET /loc`, `ac loc`) then reports, per language row **and** in the project total:
|
`GET /loc` (`ac loc`) then reports, per language row **and** in the project total:
|
||||||
|
|
||||||
- **`loc`/`sloc`** — the **total** (generated, which already includes the user exits).
|
- **`loc`/`sloc`** — the **total** (generated, which already includes the user exits).
|
||||||
- **`userExitLoc`/`userExitSloc`** — the sum of the annotated user-exit twins (the hand-written part).
|
- **`userExitLoc`/`userExitSloc`** — the sum of the annotated user-exit twins (the hand-written part).
|
||||||
@@ -239,7 +236,7 @@ existing project via `ac project update <name> -g generated_src -u user_exit`.
|
|||||||
## `?depth=` means module hops (item 65)
|
## `?depth=` means module hops (item 65)
|
||||||
|
|
||||||
On `db-accesses` / `sql-statements` (and the `?module=` scope of `variables/{name}/reads|writes`),
|
On `db-accesses` / `sql-statements` (and the `?module=` scope of `variables/{name}/reads|writes`),
|
||||||
`depth=N` means **N module calls away** — the same unit `ego_graph?depth=` and `call-tree` neighbours
|
`depth=N` means **N module calls away** — the same unit `/modules/{name}/graph?depth=` and `call-tree` neighbours
|
||||||
use. `depth=1` = the modules this one directly `CALLNAT`s, regardless of how deeply the calling
|
use. `depth=1` = the modules this one directly `CALLNAT`s, regardless of how deeply the calling
|
||||||
statement sits inside subroutines.
|
statement sits inside subroutines.
|
||||||
|
|
||||||
@@ -255,7 +252,8 @@ is now the value you'd naturally expect, and inflated values just widen the resu
|
|||||||
|
|
||||||
> **`call-tree`'s `depth` column is still raw-hop based** and mixes internal subroutines into the tree:
|
> **`call-tree`'s `depth` column is still raw-hop based** and mixes internal subroutines into the tree:
|
||||||
> a direct dependency called from the main body shows `depth=1` while one called two subroutines deep
|
> a direct dependency called from the main body shows `depth=1` while one called two subroutines deep
|
||||||
> shows `depth=3`. Use `ego_graph` when you need module-level distance. Tracked as an open roadmap item.
|
> shows `depth=3`. Use the ego graph (`/modules/{name}/graph`) when you need module-level distance. Tracked as an open
|
||||||
|
> roadmap item.
|
||||||
|
|
||||||
## Framework-mediated DB access via `INCLUDE` macros (item 44)
|
## Framework-mediated DB access via `INCLUDE` macros (item 44)
|
||||||
|
|
||||||
@@ -326,7 +324,7 @@ Natural XML wrapper subprograms build a wire payload by mapping data-area fields
|
|||||||
subroutine `COMPRESS`es `'<' #W-TAG '>' #W-VALUE`). The deep parser extracts that contract as
|
subroutine `COMPRESS`es `'<' #W-TAG '>' #W-VALUE`). The deep parser extracts that contract as
|
||||||
`PAYLOAD_FIELD` nodes and exposes it:
|
`PAYLOAD_FIELD` nodes and exposes it:
|
||||||
|
|
||||||
- `module_payload` (`GET /modules/{name}/payload`, `ac payload <module>`) → an array of
|
- `GET /modules/{name}/payload` (`ac payload <module>`) → an array of
|
||||||
`{tag, field, direction, lineNo, sourceFile}` triples. `direction` is `REQUEST` for an emitted
|
`{tag, field, direction, lineNo, sourceFile}` triples. `direction` is `REQUEST` for an emitted
|
||||||
(outbound) field. `field` is the unqualified payload field name (`WXMLIN.P-COD-USUARIO` →
|
(outbound) field. `field` is the unqualified payload field name (`WXMLIN.P-COD-USUARIO` →
|
||||||
`P-COD-USUARIO`). **`sourceFile`** is the file `lineNo` refers to — the module's own file for
|
`P-COD-USUARIO`). **`sourceFile`** is the file `lineNo` refers to — the module's own file for
|
||||||
@@ -482,7 +480,7 @@ round pulls in nothing new — so on an already-deep graph a flow query costs on
|
|||||||
traversal plus one cheap frontier check, no re-run.
|
traversal plus one cheap frontier check, no re-run.
|
||||||
|
|
||||||
**Fan-out warm (auto, on the result set).** The fan-out / traversal queries
|
**Fan-out warm (auto, on the result set).** The fan-out / traversal queries
|
||||||
`callers`, `search_identifier`, and `call-tree` also auto-deep-ingest — but on
|
`callers`, `/search/identifier`, and `call-tree` also auto-deep-ingest — but on
|
||||||
the **set of modules their result surfaced**, not a single named module. Each
|
the **set of modules their result surfaced**, not a single named module. Each
|
||||||
runs against the graph as-is, deep-ingests the surfaced modules (blocking,
|
runs against the graph as-is, deep-ingests the surfaced modules (blocking,
|
||||||
bounded by the fan-out node budget `agenticcode.deep-ingest.fanout-nodes`,
|
bounded by the fan-out node budget `agenticcode.deep-ingest.fanout-nodes`,
|
||||||
@@ -503,8 +501,8 @@ stops early and the ingest response carries a `truncation` object
|
|||||||
(`{reason: DEPTH|NODES|NODES_AND_DEPTH, maxDepth, maxNodes, hint}`) — the modules
|
(`{reason: DEPTH|NODES|NODES_AND_DEPTH, maxDepth, maxNodes, hint}`) — the modules
|
||||||
actually reached are marked `FULL`, the remainder stays as it was. Raise the
|
actually reached are marked `FULL`, the remainder stays as it was. Raise the
|
||||||
limits on an explicit module refresh to pull in more:
|
limits on an explicit module refresh to pull in more:
|
||||||
`POST /refresh/{name}?maxDepth=&maxNodes=`, MCP `refresh(module, maxDepth, maxNodes)`,
|
`POST /refresh/{name}?maxDepth=&maxNodes=`, or CLI `ac refresh <name> --max-depth --max-nodes`. Auto-triggered ingests
|
||||||
or CLI `ac refresh <name> --max-depth --max-nodes`. Auto-triggered ingests use the
|
use the
|
||||||
server defaults; if a field-level query returns partial data because the target's
|
server defaults; if a field-level query returns partial data because the target's
|
||||||
deep ingest truncated, re-run the explicit refresh with higher limits. (The
|
deep ingest truncated, re-run the explicit refresh with higher limits. (The
|
||||||
auto-trigger does not yet accept per-query limit overrides.)
|
auto-trigger does not yet accept per-query limit overrides.)
|
||||||
@@ -520,7 +518,7 @@ claim marks the module `INGESTING` and a loser waits for the winner to reach `FU
|
|||||||
`agenticcode.deep-ingest.ingesting-ttl-seconds`, default 1800; wait bounded by
|
`agenticcode.deep-ingest.ingesting-ttl-seconds`, default 1800; wait bounded by
|
||||||
`claim-wait-seconds`, default 120). A crash mid-ingest leaves the module re-triggerable
|
`claim-wait-seconds`, default 120). A crash mid-ingest leaves the module re-triggerable
|
||||||
(it never reached `FULL`), and the stale `INGESTING` is reclaimed on the next call.
|
(it never reached `FULL`), and the stale `INGESTING` is reclaimed on the next call.
|
||||||
`inspect_node` / `node_source` expose `ingestStatus`/`ingestStatusAt` on the module node.
|
`GET /nodes/{id}` / `/nodes/{id}/source` expose `ingestStatus`/`ingestStatusAt` on the module node.
|
||||||
|
|
||||||
**Warm concurrency cap (item 37).** All auto deep-ingest/warm work (by-name, fan-out,
|
**Warm concurrency cap (item 37).** All auto deep-ingest/warm work (by-name, fan-out,
|
||||||
and flow-frontier) shares a global permit pool
|
and flow-frontier) shares a global permit pool
|
||||||
@@ -548,36 +546,36 @@ origins (`http://localhost:5173`, `http://localhost:4173`) — extend the
|
|||||||
|
|
||||||
## Endpoint quick reference
|
## Endpoint quick reference
|
||||||
|
|
||||||
| Endpoint | Use for |
|
| Endpoint | Use for |
|
||||||
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `GET /modules?sourceFile=&moduleKind=&extends=` | List/filter modules; map a source file to its module name(s). Each row carries `loc`/`sloc` (item 46) and `ingestStatus`/`ingestDepth` (item 50) for status badges without a per-module round trip |
|
| `GET /modules?sourceFile=&moduleKind=&extends=` | List/filter modules; map a source file to its module name(s). Each row carries `loc`/`sloc` (item 46) and `ingestStatus`/`ingestDepth` (item 50) for status badges without a per-module round trip |
|
||||||
| `GET /loc?language=&sourceFile=` | Per-language LoC/SLoC rollup (fileCount/loc/sloc) + project total; each file counted once (item 46). For a generated/user_exit project also `userExitLoc`/`userExitSloc` + `generatedExclusiveLoc`/`generatedExclusiveSloc` (item 47) |
|
| `GET /loc?language=&sourceFile=` | Per-language LoC/SLoC rollup (fileCount/loc/sloc) + project total; each file counted once (item 46). For a generated/user_exit project also `userExitLoc`/`userExitSloc` + `generatedExclusiveLoc`/`generatedExclusiveSloc` (item 47) |
|
||||||
| `GET /modules/{name}/digest` | Tiny triage view before deciding which modules to expand |
|
| `GET /modules/{name}/digest` | Tiny triage view before deciding which modules to expand |
|
||||||
| `GET /modules/{name}/context` | One-shot overview: functions, callers, callees, DB accesses, SQL/variable summaries (`?include=` for full lists) |
|
| `GET /modules/{name}/context` | One-shot overview: functions, callers, callees, DB accesses, SQL/variable summaries (`?include=` for full lists) |
|
||||||
| `GET /modules/{name}/callers` \| `/callees` | Direct callers/callees incl. `EXTENDS`/`IMPLEMENTS`/`INJECTS`/`REFERENCES`. `callers` `scope`: **`external` (default)** = modules that call this one (CALLNAT/inheritance), **rolled up to the calling MODULE**: a call made from inside a subroutine/method is attributed to its owning module (never the calling `FUNCTION` node), and repeated call sites from one caller collapse to a single row whose `sites` list every line — symmetric with how `callees` anchors its source side. `internal` = the module's own subroutines' `PERFORM` wiring (function-level). The default is external-only, module-typed only, and never lists the module as its own caller (no `MODULE→MODULE` self-loop); use `scope=internal` or `/functions/{fn}/callers` for intra-module / function-level wiring. `callees` is unchanged (default lists both external CALLNAT and internal PERFORM targets) |
|
| `GET /modules/{name}/callers` \| `/callees` | Direct callers/callees incl. `EXTENDS`/`IMPLEMENTS`/`INJECTS`/`REFERENCES`. `callers` `scope`: **`external` (default)** = modules that call this one (CALLNAT/inheritance), **rolled up to the calling MODULE**: a call made from inside a subroutine/method is attributed to its owning module (never the calling `FUNCTION` node), and repeated call sites from one caller collapse to a single row whose `sites` list every line — symmetric with how `callees` anchors its source side. `internal` = the module's own subroutines' `PERFORM` wiring (function-level). The default is external-only, module-typed only, and never lists the module as its own caller (no `MODULE→MODULE` self-loop); use `scope=internal` or `/functions/{fn}/callers` for intra-module / function-level wiring. `callees` is unchanged (default lists both external CALLNAT and internal PERFORM targets) |
|
||||||
| `GET /modules/{name}/functions/{function}/callers` | **FUNCTION-level callers** (item 52): who `PERFORM`s (Natural) or calls (Java cross-class) a specific subroutine/method, with call-site `lineNos`. Finer-grained than the module-level `/callers` (which is module→module). Same `CallRefResponse` shape. MCP `function_callers`, CLI `ac function-callers <module> <function>` |
|
| `GET /modules/{name}/functions/{function}/callers` | **FUNCTION-level callers** (item 52): who `PERFORM`s (Natural) or calls (Java cross-class) a specific subroutine/method, with call-site `lineNos`. Finer-grained than the module-level `/callers` (which is module→module). Same `CallRefResponse` shape. CLI `ac function-callers <module> <function>` |
|
||||||
| `GET /modules/{name}/call-tree?depth=` | Transitive call graph to scope a feature |
|
| `GET /modules/{name}/call-tree?depth=` | Transitive call graph to scope a feature |
|
||||||
| `GET /dynamic-calls/unresolved` \| `/overrides` · `POST`/`DELETE /overrides` | **Manual dynamic-`CALLNAT` overrides (item 82).** `unresolved` lists open `CALLNAT <var>` sites `{module, originFile, lineNo, variable}`; `POST /overrides {originFile, lineNo, targets[], variable?, note?}` pins a site to real module(s) (applied at once, persisted across refreshes, `400 UNKNOWN_TARGET` for a non-module); `DELETE /overrides?originFile=&lineNo=` resets one site (omit both = all) and restores the placeholder inline; `GET /overrides` lists them with an `obsolete` flag. MCP `list_unresolved_dynamic_calls`/`list_dynamic_call_overrides`/`set_dynamic_call_override`/`reset_dynamic_call_override`, CLI `ac dynamic-calls unresolved\|overrides\|set\|reset` |
|
| `GET /dynamic-calls/unresolved` \| `/overrides` · `POST`/`DELETE /overrides` | **Manual dynamic-`CALLNAT` overrides (item 82).** `unresolved` lists open `CALLNAT <var>` sites `{module, originFile, lineNo, variable}`; `POST /overrides {originFile, lineNo, targets[], variable?, note?}` pins a site to real module(s) (applied at once, persisted across refreshes, `400 UNKNOWN_TARGET` for a non-module); `DELETE /overrides?originFile=&lineNo=` resets one site (omit both = all) and restores the placeholder inline; `GET /overrides` lists them with an `obsolete` flag. CLI `ac dynamic-calls unresolved\|overrides\|set\|reset` |
|
||||||
| `GET /modules/{name}/graph?direction=&depth=&limit=` | Ego graph (item 49): bounded module-level call neighbourhood as **nodes + edges** (unlike call-tree). `direction` = `out`/`in`/`both`; `limit` caps nodes (BFS order) and sets `truncated`; unresolved targets carry `unresolved=true` + empty `sourceFile`. MCP `ego_graph`, CLI `ac ego-graph` |
|
| `GET /modules/{name}/graph?direction=&depth=&limit=` | Ego graph (item 49): bounded module-level call neighbourhood as **nodes + edges** (unlike call-tree). `direction` = `out`/`in`/`both`; `limit` caps nodes (BFS order) and sets `truncated`; unresolved targets carry `unresolved=true` + empty `sourceFile`. CLI `ac ego-graph` |
|
||||||
| `GET /modules/{name}/db-accesses` \| `/sql-statements` | DB tables + mode, raw statement text (pass `?depth=` for Natural). **`db-accesses`/`workfile-accesses` return every row when no `limit` is given (item 103)** — they used to default to 50, and since the response is a bare array with no total and no `truncated` flag the cut was invisible: `WGEAGB0S?depth=10` returned 50 of 64 rows and hid 7 tables outright. An explicit `limit` is still honoured exactly. `db-accesses` items carry **`sites: [{lineNo, sourceFile, viaCopycode, includedAt}]`** (+ kept `lineNos`); `sql-statements` items carry **`sourceFile`** + **`viaCopycode`** — so a copycode-sourced access (e.g. `SELECT … FROM SYSIBM-SYSDUMMY1` in `USIX043C.cpy`) reports the `.cpy` line, not a bare number that reads as a host-file line |
|
| `GET /modules/{name}/db-accesses` \| `/sql-statements` | DB tables + mode, raw statement text (pass `?depth=` for Natural). **`db-accesses`/`workfile-accesses` return every row when no `limit` is given (item 103)** — they used to default to 50, and since the response is a bare array with no total and no `truncated` flag the cut was invisible: `WGEAGB0S?depth=10` returned 50 of 64 rows and hid 7 tables outright. An explicit `limit` is still honoured exactly. `db-accesses` items carry **`sites: [{lineNo, sourceFile, viaCopycode, includedAt}]`** (+ kept `lineNos`); `sql-statements` items carry **`sourceFile`** + **`viaCopycode`** — so a copycode-sourced access (e.g. `SELECT … FROM SYSIBM-SYSDUMMY1` in `USIX043C.cpy`) reports the `.cpy` line, not a bare number that reads as a host-file line |
|
||||||
| `GET /modules/{name}/workfile-accesses` | Natural **work files** (sequential/flat-file I/O — `READ`/`WRITE WORK FILE n`), the work-file analogue of `db-accesses` (item 84): `[{workFile, physicalName, mode: READS\|WRITES, recordBuffers, lineNos, sites}]`, aggregated per work-file number + mode. `sites: [{lineNo, sourceFile, viaCopycode, includedAt}]` gives each access its file context (copycode-aware), like `db-accesses`. `physicalName` comes from a `DEFINE WORK FILE n '<name>'`, else `null`. **Kept separate from `db-accesses`** — a work file is not an ADABAS/SQL table (fixes a former bug where `READ WORK FILE` created a phantom `DB_TABLE 'WORK'`). MCP `workfile_accesses`, CLI `ac workfile-accesses <module>` |
|
| `GET /modules/{name}/workfile-accesses` | Natural **work files** (sequential/flat-file I/O — `READ`/`WRITE WORK FILE n`), the work-file analogue of `db-accesses` (item 84): `[{workFile, physicalName, mode: READS\|WRITES, recordBuffers, lineNos, sites}]`, aggregated per work-file number + mode. `sites: [{lineNo, sourceFile, viaCopycode, includedAt}]` gives each access its file context (copycode-aware), like `db-accesses`. `physicalName` comes from a `DEFINE WORK FILE n '<name>'`, else `null`. **Kept separate from `db-accesses`** — a work file is not an ADABAS/SQL table (fixes a former bug where `READ WORK FILE` created a phantom `DB_TABLE 'WORK'`). CLI `ac workfile-accesses <module>` |
|
||||||
| `GET /modules/{name}/data-structures` | Which copybooks/inline groups a module uses. A `USING <member>` binds by **member (file) name**, never by a level-1 record inside the file (item 100) — before that, `WGEAGB0S USING W-WIF-A2` reported `old/W-WIF-A7.pda` (whose level-1 record is a copy-pasted `1W-WIF-A2`), and a data area with several level-1 records and none named after the member (`VLAYERLA.lda`, `USIX020L.lda`) resolved to nothing at all (`sourceFile: null`, `area: UNKNOWN`, `fieldCount: 0`) although the file was ingested. One row per resolved definition, `(name, sourceFile)` (item 102) — never one row blending an arbitrary file with another definition's `fieldCount` |
|
| `GET /modules/{name}/data-structures` | Which copybooks/inline groups a module uses. A `USING <member>` binds by **member (file) name**, never by a level-1 record inside the file (item 100) — before that, `WGEAGB0S USING W-WIF-A2` reported `old/W-WIF-A7.pda` (whose level-1 record is a copy-pasted `1W-WIF-A2`), and a data area with several level-1 records and none named after the member (`VLAYERLA.lda`, `USIX020L.lda`) resolved to nothing at all (`sourceFile: null`, `area: UNKNOWN`, `fieldCount: 0`) although the file was ingested. One row per resolved definition, `(name, sourceFile)` (item 102) — never one row blending an arbitrary file with another definition's `fieldCount` |
|
||||||
| `GET /modules/{name}/payload` | Natural XML wire-payload contract: `{tag, field, direction, source, lineNo, sourceFile}` — static `ADD-XML-LINE` idiom (`source=IDIOM`, item 45) or derived from the wrapper's interface PDA (`source=PDA`, item 46b). `sourceFile` is the file `lineNo` refers to (module for IDIOM, PDA for PDA) |
|
| `GET /modules/{name}/payload` | Natural XML wire-payload contract: `{tag, field, direction, source, lineNo, sourceFile}` — static `ADD-XML-LINE` idiom (`source=IDIOM`, item 45) or derived from the wrapper's interface PDA (`source=PDA`, item 46b). `sourceFile` is the file `lineNo` refers to (module for IDIOM, PDA for PDA) |
|
||||||
| `GET /modules/{name}/dispatch-table` | Natural `DECIDE ON VALUE OF` routing table |
|
| `GET /modules/{name}/dispatch-table` | Natural `DECIDE ON VALUE OF` routing table |
|
||||||
| `GET /modules/{name}/functions?kind=` \| `/functions/{fn}/overrides` \| `/functions/overrides` | Method list, modifier filter (Java), subclass overrides (single/bulk). Each item carries **`sourceFile`** + **`viaCopycode`** (item 84): a Natural subroutine pulled in via `INCLUDE` reports the **copycode** file and `viaCopycode:true`, so its `startLine`/`endLine` are read as offsets into that copycode — **not** into the including module's own file (which is shorter). `viaCopycode:false` = declared inline. Always `false` for Java |
|
| `GET /modules/{name}/functions?kind=` \| `/functions/{fn}/overrides` \| `/functions/overrides` | Method list, modifier filter (Java), subclass overrides (single/bulk). Each item carries **`sourceFile`** + **`viaCopycode`** (item 84): a Natural subroutine pulled in via `INCLUDE` reports the **copycode** file and `viaCopycode:true`, so its `startLine`/`endLine` are read as offsets into that copycode — **not** into the including module's own file (which is shorter). `viaCopycode:false` = declared inline. Always `false` for Java |
|
||||||
| `GET /data-structures/{name}/fields` \| `/db-tables/{name}/columns` \| `/modules/{name}/columns` | Field/column schemas for DTO/entity generation. Every field carries **`sourceFile`** (item 101). When a structure name resolves to several definitions (42 level-1 names recur across `upms` data areas), the **member root** — the definition whose file basename equals the name, i.e. what a `USING <member>` binds to — wins; **`?sourceFile=`** pins a specific one. Before item 101 the definitions were silently unioned: `W-WIF-A2` returned 15 fields, the merge of `W-WIF-A2.pda` (5) and `W-WIF-A7.pda` (10), a layout that exists nowhere |
|
| `GET /data-structures/{name}/fields` \| `/db-tables/{name}/columns` \| `/modules/{name}/columns` | Field/column schemas for DTO/entity generation. Every field carries **`sourceFile`** (item 101). When a structure name resolves to several definitions (42 level-1 names recur across `upms` data areas), the **member root** — the definition whose file basename equals the name, i.e. what a `USING <member>` binds to — wins; **`?sourceFile=`** pins a specific one. Before item 101 the definitions were silently unioned: `W-WIF-A2` returned 15 fields, the merge of `W-WIF-A2.pda` (5) and `W-WIF-A7.pda` (10), a layout that exists nowhere |
|
||||||
| `GET /variables/{name}/reads` \| `/writes` \| `/flow-forward` \| `/flow-backward` \| `/field-flow` | Impact analysis and dataflow tracing |
|
| `GET /variables/{name}/reads` \| `/writes` \| `/flow-forward` \| `/flow-backward` \| `/field-flow` | Impact analysis and dataflow tracing |
|
||||||
| `GET /search/identifier` \| `/search/value` \| `/search/annotation` | Cross-project lookup by name / literal value / annotation. `search/identifier` matches the **exact** declared name but is **sigil-insensitive**: a leading Natural sigil (`#` user, `&` AIV, `+` GDA) is ignored on both sides, so `name=K-OUT-MAX` finds the declared `#K-OUT-MAX` (and vice-versa). Optional **scope** filters `sourceFile=<relpath>` and `module=<name>` (item 53) narrow the match to one file / one module — use them to pinpoint a module-local declaration when a name recurs across dozens of modules (the result is otherwise paginated and the local one may fall off the page). To keep the **full cross-project list** yet still guarantee a given module's own declaration is on the first page, pass `priorityModule=<name>` instead of `module=`: it does not filter, but pins that module's matches to the front (ahead of the otherwise `sourceFile`-ordered rest) so they survive the `limit`. This is what the web UI's click-to-identify sends for the open module. MCP `search_identifier` / CLI `ac search-identifier --module --priority-module --source-file --type` accept the same filters. **Latency (item 105):** a lookup whose hits lie in a Natural data area used to take 60-75 s — every fan-out query deep-ingested the surfaced `.lda`/`.pda`, which can never reach `FULL` (a data area yields no `MODULE` node), so it was re-warmed on every call and each warm dragged a whole-project finalize behind it. Data areas are now excluded from the fan-out warm; they have no deep tier to gain |
|
| `GET /search/identifier` \| `/search/value` \| `/search/annotation` | Cross-project lookup by name / literal value / annotation. `search/identifier` matches the **exact** declared name but is **sigil-insensitive**: a leading Natural sigil (`#` user, `&` AIV, `+` GDA) is ignored on both sides, so `name=K-OUT-MAX` finds the declared `#K-OUT-MAX` (and vice-versa). Optional **scope** filters `sourceFile=<relpath>` and `module=<name>` (item 53) narrow the match to one file / one module — use them to pinpoint a module-local declaration when a name recurs across dozens of modules (the result is otherwise paginated and the local one may fall off the page). To keep the **full cross-project list** yet still guarantee a given module's own declaration is on the first page, pass `priorityModule=<name>` instead of `module=`: it does not filter, but pins that module's matches to the front (ahead of the otherwise `sourceFile`-ordered rest) so they survive the `limit`. This is what the web UI's click-to-identify sends for the open module. CLI `ac search-identifier --module --priority-module --source-file --type` accept the same filters. **Latency (item 105):** a lookup whose hits lie in a Natural data area used to take 60-75 s — every fan-out query deep-ingested the surfaced `.lda`/`.pda`, which can never reach `FULL` (a data area yields no `MODULE` node), so it was re-warmed on every call and each warm dragged a whole-project finalize behind it. Data areas are now excluded from the fan-out warm; they have no deep tier to gain |
|
||||||
| `GET /search/source?regex=&limit=&ignoreCase=` (MCP `search_source`, `ac search-source`) | Regex **grep over module source text** (item 54): `{module, sourceFile, lineNo, line}` hits + `truncated`. Case-insensitive by default. Complements `search_identifier` (declared names) — use for code patterns (statements, table names, literals) |
|
| `GET /search/source?regex=&limit=&ignoreCase=` (`ac search-source`) | Regex **grep over module source text** (item 54): `{module, sourceFile, lineNo, line}` hits + `truncated`. Case-insensitive by default. Complements `/search/identifier` (declared names) — use for code patterns (statements, table names, literals) |
|
||||||
| `GET /nodes/{id}` | Every property of one node (when a curated DTO is missing something) |
|
| `GET /nodes/{id}` | Every property of one node (when a curated DTO is missing something) |
|
||||||
| `GET /nodes/{id}/source` \| `/modules/{name}/source` \| `/source?file=` | Source text — **only when you have no other access to the source** (you always do in this repo, see "Reading source in this repo" above). `module_source` returns the **whole file** when the line range is omitted (M1), or a `[startLine,endLine]` slice when both are given. `/source?file=<relpath>` (MCP `file_source`, CLI `ac file-source`) serves a file by **relative path** rather than module name — for files that aren't standalone modules, e.g. a Natural data area (PDA/LDA) USING'd by a module, whose field line numbers refer to that file. Same whole-file/range + stale-source semantics; the client-supplied path is rejected (`400 INVALID_SOURCE_FILE`) if it escapes the project root |
|
| `GET /nodes/{id}/source` \| `/modules/{name}/source` \| `/source?file=` | Source text — **only when you have no other access to the source** (you always do in this repo, see "Reading source in this repo" above). `/modules/{name}/source` returns the **whole file** when the line range is omitted (M1), or a `[startLine,endLine]` slice when both are given. `/source?file=<relpath>` (CLI `ac file-source`) serves a file by **relative path** rather than module name — for files that aren't standalone modules, e.g. a Natural data area (PDA/LDA) USING'd by a module, whose field line numbers refer to that file. Same whole-file/range + stale-source semantics; the client-supplied path is rejected (`400 INVALID_SOURCE_FILE`) if it escapes the project root |
|
||||||
|
|
||||||
Full endpoint list, request params, and response field details:
|
Full endpoint list, request params, and response field details:
|
||||||
`x-docs/agent-api-system-prompt.md`.
|
`x-docs/agent-api-system-prompt.md`.
|
||||||
|
|
||||||
## Missing capability?
|
## Missing capability?
|
||||||
|
|
||||||
If the API/MCP/CLI genuinely cannot answer a question (not just
|
If the API/CLI genuinely cannot answer a question (not just
|
||||||
unreachable — the capability doesn't exist), finish the task via
|
unreachable — the capability doesn't exist), finish the task via
|
||||||
grep/Explore as a fallback, then use `AskUserQuestion` to flag the gap and
|
grep/Explore as a fallback, then use `AskUserQuestion` to flag the gap and
|
||||||
ask whether it should become a roadmap item in `x-docs/roadmap.md`. Don't
|
ask whether it should become a roadmap item in `x-docs/roadmap.md`. Don't
|
||||||
|
|||||||
@@ -144,4 +144,3 @@ mapping, business logic, and a complete picture of what else is affected.
|
|||||||
| Java parser | JavaParser library |
|
| Java parser | JavaParser library |
|
||||||
| API | JAX-RS / RESTEasy Reactive |
|
| API | JAX-RS / RESTEasy Reactive |
|
||||||
| Tests | JUnit 5 + RestAssured + Testcontainers |
|
| Tests | JUnit 5 + RestAssured + Testcontainers |
|
||||||
| MCP integration | In progress |
|
|
||||||
|
|||||||
@@ -957,25 +957,12 @@ lands in both ingest tiers at once.)*
|
|||||||
`DynamicCallnatFoldIT` (fold resolves `YABALKEY`+`GN0@6/3` → `YABALGN0`; manual override wins);
|
`DynamicCallnatFoldIT` (fold resolves `YABALKEY`+`GN0@6/3` → `YABALGN0`; manual override wins);
|
||||||
full dynamic-callnat regression 89/89 green. Docs in `mcp-api-usage-ac-implementation.md`.
|
full dynamic-callnat regression 89/89 green. Docs in `mcp-api-usage-ac-implementation.md`.
|
||||||
|
|
||||||
- [ ] **26. MCP session reliability (POSTPONED 2026-07-13)** (investigated 2026-07-07, not fixed —
|
- [x] **26. MCP session reliability — RESOLVED BY REMOVAL (2026-08-04)** (investigated 2026-07-07,
|
||||||
see below) — `mcp__agenticcode__module_functions` (and potentially other MCP
|
reproduced 2026-08-02, never fixed). `mcp__agenticcode__*` calls intermittently — and in the
|
||||||
tools) intermittently failed with `"the first message from the client must be
|
2026-08-02 session, from the very first call — failed with `"the first message from the client must
|
||||||
initialize: tools/call"` after a sequence of prior successful MCP calls in the
|
be initialize: tools/call"`, forcing every playbook step to be re-expressed as `curl`. The evidence
|
||||||
same session, forcing a fallback to the equivalent REST endpoint. Findings:
|
pointed at the MCP client's reconnect handling rather than a server-side bug this codebase's config
|
||||||
no session/idle-timeout is configured server-side
|
could fix, and the REST endpoints answered normally throughout. **Decision 2026-08-04: the MCP
|
||||||
(`quarkus.http.idle-timeout` unset), and `application.properties` only sets
|
server surface was removed entirely** rather than debugged — see "MCP surface removed" in
|
||||||
`quarkus.mcp.server.server-info.*`. The error shape (client sent `tools/call`
|
`x-docs/features.md`. REST + `ac` CLI are now the only access paths; all 40 former tools had a REST
|
||||||
without a fresh `initialize`) points at the **MCP client's** reconnect
|
twin, so no capability was lost.
|
||||||
handling on a new SSE stream, not a server-side bug this codebase's config
|
|
||||||
can fix. Left open pending either a `quarkus-mcp-server-sse` version bump
|
|
||||||
with related fixes, or evidence this is in fact server-triggered.
|
|
||||||
|
|
||||||
**2026-08-02 — reproduced, and worse than "intermittent" in this session.** Every MCP call failed with
|
|
||||||
the same message from the *first* call onwards (`mcp__agenticcode__module_dispatch_table`, then
|
|
||||||
`mcp__agenticcode__list_projects`), so the whole `upms` webservice audit ran over REST via `curl`
|
|
||||||
instead. Two observations that may narrow it: (a) it was not a degradation after a run of successful
|
|
||||||
calls — the very first tool call in the session failed, which argues against an idle/reconnect timeout
|
|
||||||
and for the session never being established; (b) the equivalent REST endpoints answered normally
|
|
||||||
throughout, so the server and the graph were healthy. Practical impact for agent work: the documented
|
|
||||||
MCP surface is unusable in these sessions and every playbook step has to be re-expressed as `curl`,
|
|
||||||
which is why the REST examples in `agent-api-system-prompt.md` earn their keep.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user