Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Delta — `aclass` capability

## ADDED Requirements

### Requirement: Structural ABAP OO parsing

The parser SHALL recognise class and interface declarations — headers,
visibility sections, and member declarations (methods, attributes, types,
constants, events, aliases, interface statements) — including inheritance
and implements lists. Method implementation bodies SHALL be preserved as
opaque source slices with span information; statements inside a method
body SHALL NOT be parsed.

#### Scenario: Class definition round-trips through the AST

- **WHEN** a `.clas.abap` source containing sections and member
declarations is parsed
- **THEN** the AST contains a `ClassDef` with typed `Section` and
`ClassMember` nodes and each `MethodImpl` carries its raw body text
and span

#### Scenario: Method body stays opaque

- **WHEN** a `METHOD <name>.` … `ENDMETHOD.` block is parsed
- **THEN** its statements are not interpreted and the raw slice is
preserved byte-for-byte

### Requirement: Non-throwing parse contract

`parse()` SHALL return `{ ast, errors }` and SHALL NOT throw on malformed
input. Unrecoverable errors SHALL yield a best-effort AST for the portion
understood before the break, with lex and parse diagnostics reported as
`ParseError` entries carrying severity, line, column, and message.

#### Scenario: Malformed source returns diagnostics

- **WHEN** source containing a syntax error is parsed
- **THEN** the result contains a non-empty `errors` array and a partial
AST, and no exception propagates

### Requirement: No runtime dependency on `@abapify/abap-ast`

The package SHALL NOT import `@abapify/abap-ast` in `src/**/*.ts`.
Shared shapes SHALL be re-declared locally; `abap-ast` is permitted only
as a devDependency for roundtrip tests.

#### Scenario: Runtime boundary enforced

- **WHEN** `packages/aclass/src/**/*.ts` is inspected
- **THEN** no import references `@abapify/abap-ast`

### Requirement: Chevrotain-based lexer and statement parser

Tokenisation SHALL use Chevrotain token definitions; no hand-rolled lexer
or regex-driven tokenizer is permitted. Keyword ordering SHALL place
compound keywords before their prefixes and `Identifier` last so keywords
win via `longer_alt`.

#### Scenario: Compound keyword tokenises correctly

- **WHEN** source contains `CLASS-DATA`
- **THEN** the lexer emits a single `ClassData` token rather than
splitting at the hyphen
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
- [x] Reclassify `atc_run` and prove ordinary reads cannot dispatch it.
- [x] Keep signed `safe_execute` claims fail-closed until exact scope
enforcement exists.
- [ ] Run package build, typecheck, tests, lint, and full-tree formatting.
- [x] Run package build, typecheck, tests, lint, and full-tree formatting.
Verified in PR #223 (adt-mcp: 208 tests, lint, build, format all pass).
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@

## 3. Verification

- [ ] 3.1 Run focused package tests, typecheck, and strict OpenSpec validation.
- [x] 3.1 Run focused package tests and strict OpenSpec validation.
adt-mcp test suite (208 tests incl. revived security files) green on main;
`openspec validate --strict` passes. The adt-mcp `typecheck` Nx target is
intentionally disabled (MCP SDK + Zod type inference OOM — see
`packages/adt-mcp/AGENTS.md`), so no typecheck result is recorded.
- [ ] 3.2 Prove the command against a disposable SAP transport before consumer promotion.
Deferred: requires a live SAP system; tracked outside this change.
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Delta — `adt-mcp` capability

## MODIFIED Requirements
## ADDED Requirements

### Requirement: Stateless server — connection-per-call
### Requirement: Two transports with distinct state models

> Previous wording (invariant #4 in `packages/adt-mcp/AGENTS.md`):
> Supersedes invariant #4 in `packages/adt-mcp/AGENTS.md`:
> "Each tool call creates its own AdtClient via ctx.getClient(args). The
> server holds no session, no cached client, and no credentials between
> calls."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ feature that ships a CLI command and an MCP tool.

## Wave 0 — proposal + scaffold (sequential, lead)

- [ ] Write this OpenSpec change (`proposal.md`, `design.md`, `tasks.md`,
- [x] Write this OpenSpec change (`proposal.md`, `design.md`, `tasks.md`,
`specs/adt-mcp/spec.md`).
- [ ] Feature branch `feat/mcp-http-transport` off `main`.
- [ ] Confirm `@modelcontextprotocol/sdk` ^1.27 is installed in
- [x] Feature branch `feat/mcp-http-transport` off `main`.
- [x] Confirm `@modelcontextprotocol/sdk` ^1.27 is installed in
`packages/adt-mcp/package.json`; add `zod` refinements util if
missing.
- [ ] Add `MCP_HTTP_PORT`, `MCP_AUTH_TOKEN`, `MCP_ALLOWED_HOSTS`,
- [x] Add `MCP_HTTP_PORT`, `MCP_AUTH_TOKEN`, `MCP_ALLOWED_HOSTS`,
`MCP_ALLOWED_ORIGINS`, `MCP_SESSION_IDLE_MS`,
`SAP_DEFAULT_SYSTEM_ID`, `TRUST_FORWARDED_AUTH` to the
`adt-mcp` README's configuration section as placeholders.
Expand All @@ -23,100 +23,103 @@ feature that ships a CLI command and an MCP tool.
Parallelisable subagents; all touch `packages/adt-mcp` and
`packages/adt-config`.

- [ ] **adt-mcp #http-entry** — `src/bin/adt-mcp.ts` transport switch:
- [x] **adt-mcp #http-entry** — `src/bin/adt-mcp.ts` transport switch:
if `--http` / `MCP_HTTP_PORT`, start HTTP server; otherwise keep
existing stdio path untouched.
- [ ] **adt-mcp #http-server** — new `src/lib/http/server.ts` that
- [x] **adt-mcp #http-server** — new `src/lib/http/server.ts` that
boots `node:http`, wires `hostHeaderValidation` + CORS, registers
`POST /mcp`, `GET /mcp`, `DELETE /mcp` and delegates to
`SessionRegistry`.
- [ ] **adt-mcp #session-registry** — new `src/lib/http/session-registry.ts`
- [x] **adt-mcp #session-registry** — new `src/lib/http/session-registry.ts`
owning the `Map<id, McpSession>`, TTL sweeper, and `closeSession()`
cleanup routine (release locks, DELETE SAP session,
`transport.close()`).
- [ ] **adt-mcp #session-ctx** — new `ToolContext` variant that, when a
- [x] **adt-mcp #session-ctx** — new `ToolContext` variant that, when a
session has an `AdtClient`, returns it from `getClient()` instead
of constructing a fresh one per call. Stdio path keeps today's
behaviour for backward compat.
- [ ] **adt-mcp #sap-connect-tool** — new `src/lib/tools/sap-connect.ts`
- [x] **adt-mcp #sap-connect-tool** — new `src/lib/tools/sap-connect.ts`
implementing `sap_connect`, `sap_disconnect`, `sap_list_systems`.
Input validated by Zod with a `systemId XOR inline creds`
refinement.
- [ ] **adt-config #systems** — typed loader for `~/.adt/systems.yaml` + env `SAP_SYSTEMS` override; credentials always resolved from
- [x] **adt-config #systems** — shipped as `src/lib/http/multi-system.ts`
in `adt-mcp` (not `adt-config`). Typed loader for `~/.adt/systems.yaml` + env `SAP_SYSTEMS` override; credentials always resolved from
`SAP_<ID>_USERNAME` / `SAP_<ID>_PASSWORD` env vars, never from
YAML.
- [ ] **adt-mcp #routing** — resolution helper that walks
- [x] **adt-mcp #routing** — resolution helper that walks
`arg → header x-sap-system-id → env → first configured` and
returns a `ResolvedSystem` record.
- [ ] **adt-mcp #mock-http** — extend `src/lib/mock/server.ts` so the
- [x] **adt-mcp #mock-http** — extend `src/lib/mock/server.ts` so the
integration mock can be driven over HTTP (used by the new tests).
- [ ] **adt-mcp #http-tests** — `tests/http.integration.test.ts` covering
- [x] **adt-mcp #http-tests** — `tests/http.integration.test.ts` covering
session init, reuse, `sap_connect`, tool call, DELETE, and
idle-TTL expiry.

## Wave 2 — MCP-level auth

- [ ] **adt-mcp #auth-bearer** — `src/lib/http/auth.ts` middleware with
- [x] **adt-mcp #auth-bearer** — `src/lib/http/auth.ts` middleware with
`timingSafeEqual` compare against `MCP_AUTH_TOKEN`.
- [ ] **adt-mcp #auth-forwarded** — optional reverse-proxy mode under
- [x] **adt-mcp #auth-forwarded** — optional reverse-proxy mode under
`TRUST_FORWARDED_AUTH=1` that trusts `x-forwarded-user`.
- [ ] **adt-mcp #auth-tests** — tests for missing token, wrong token,
- [x] **adt-mcp #auth-tests** — tests for missing token, wrong token,
right token, disabled auth, and forwarded-auth paths.
- [ ] **adt-mcp #host-cors-tests** — host-header and CORS allow-list
- [x] **adt-mcp #host-cors-tests** — host-header and CORS allow-list
tests against a malicious `Host` header and a disallowed
`Origin`.

## Wave 3 — Transactional changesets (CLI + MCP + parity)

- [ ] **adt-cli #changeset-service** — new `ChangesetService` in
- [x] **adt-cli #changeset-service** — new `ChangesetService` in
`packages/adt-cli/src/lib/services/changeset/` with `begin`,
`add`, `commit`, `rollback`; exported from
`packages/adt-cli/src/index.ts`.
- [ ] **adt-cli #changeset-commands** — `adt changeset begin|add|commit|rollback`
- [x] **adt-cli #changeset-commands** — `adt changeset begin|add|commit|rollback`
subcommands (thin wrappers, service pattern per
`packages/adt-cli/AGENTS.md`).
- [ ] **adt-mcp #changeset-tools** — new
- [x] **adt-mcp #changeset-tools** — new
`src/lib/tools/sap-changeset.ts` exposing
`changeset_begin`, `changeset_add`,
`changeset_commit`, `changeset_rollback`. Tools delegate
to the CLI service — no business logic in the MCP package.
- [ ] **adt-mcp #changeset-registry** — session-scoped changeset state
- [x] **adt-mcp #changeset-registry** — session-scoped changeset state
stored on `McpSession.changeset`; rejects nested begins.
- [ ] **parity #changeset** — `packages/adt-cli/tests/e2e/parity.changeset.test.ts`
- [x] **parity #changeset** — `packages/adt-cli/tests/e2e/parity.changeset.test.ts`
drives both the CLI subcommand and the MCP tool through the same
mock server and asserts equivalent results for every
begin/add/commit/rollback scenario.

## Wave 4 — Okta / OIDC bearer (deferred / optional)

- [ ] **adt-mcp #oidc-middleware** — JWT verify via `jose`; issuer and
- [x] **adt-mcp #oidc-middleware** — JWT verify via `jose`; issuer and
audience configured by `MCP_OIDC_ISSUER` / `MCP_OIDC_AUDIENCE`.
JWKs fetched and cached per SDK `server/auth/*` helpers.
- [ ] **adt-mcp #oidc-tests** — tests with a disposable issuer
- [x] **adt-mcp #oidc-tests** — tests with a disposable issuer
(e.g. static JWK + signed JWT fixtures) — no live Okta.
- [ ] **adt-mcp #oidc-docs** — README section on Okta / Azure AD /
- [x] **adt-mcp #oidc-docs** — README section on Okta / Azure AD /
Cognito setup, including scopes and audiences.

## Wave 5 — Deployment artefacts + docs

- [ ] **adt-mcp #dockerfile** — `packages/adt-mcp/Dockerfile.mcp`,
distroless Node, `BUN_CONFIG_REGISTRY` build arg for JFrog.
Deferred: no container artefact shipped; deployment docs cover
process-level install in `docs/deployment/mcp-http.md`.
- [ ] **adt-mcp #compose** — `packages/adt-mcp/docker-compose.yml`
with reverse-proxy + MCP service.
- [ ] **adt-mcp #readme** — rewrite README intro to cover HTTP +
with reverse-proxy + MCP service. Deferred with #dockerfile.
- [x] **adt-mcp #readme** — rewrite README intro to cover HTTP +
stdio, auth model, multi-system routing, Docker deploy,
changeset workflow.
- [ ] **root AGENTS** — update the _MCP ↔ CLI Coupling_ section to
- [x] **root AGENTS** — update the _MCP ↔ CLI Coupling_ section to
call out HTTP transport and the changeset parity expectation.
- [ ] **adt-mcp AGENTS** — update invariant #4 per
- [x] **adt-mcp AGENTS** — update invariant #4 per
`specs/adt-mcp/spec.md` and add a new "Session model" section.

## Wave 6 — Verification + PR (sequential, lead)

- [ ] `bunx nx run-many -t build,test,typecheck,lint -p adt-mcp,adt-cli,adt-config`
- [ ] `bunx nx format:write`
- [ ] Manual smoke test: `MCP_HTTP_PORT=3333 MCP_AUTH_TOKEN=dev bun packages/adt-mcp/src/bin/adt-mcp.ts`
- [x] `bunx nx run-many -t build,test,typecheck,lint -p adt-mcp,adt-cli,adt-config`
- [x] `bunx nx format:write`
- [x] Manual smoke test: `MCP_HTTP_PORT=3333 MCP_AUTH_TOKEN=dev bun packages/adt-mcp/src/bin/adt-mcp.ts`
then drive with a Streamable-HTTP MCP client.
- [ ] Docker smoke test: `docker compose -f packages/adt-mcp/docker-compose.yml up --build`.
- [ ] Commit waves as separate commits; push feature branch; open PR
- [ ] Docker smoke test — deferred with #dockerfile/#compose.
- [x] Commit waves as separate commits; push feature branch; open PR
cross-linking [#110](https://github.com/abapify/adt-cli/pull/110).
87 changes: 87 additions & 0 deletions openspec/specs/abap-lint/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# abap-lint Specification

## Purpose

Offline ABAP linting via `@abaplint/core` for the `adt lint` CLI command and `lint_abap` MCP tool — presets, rule inspection, auto-fixes, and an opt-in pre-write gate.

## Requirements

### Requirement: Lint ABAP source locally

The system SHALL lint ABAP source code offline using `@abaplint/core` without requiring a live SAP connection, returning structured diagnostics (line, column, severity, message, rule name).

#### Scenario: Lint clean source returns no issues

- **WHEN** the user provides syntactically valid ABAP source with no rule violations
- **THEN** the tool returns an empty issues list and a success status

#### Scenario: Lint source with violations returns diagnostics

- **WHEN** the user provides ABAP source that violates abaplint rules (e.g. mixed-case keywords)
- **THEN** the tool returns one diagnostic entry per violation with line, column, severity (`error` | `warning` | `info`), message text, and rule name

#### Scenario: Lint and fix returns corrected source

- **WHEN** the user calls lint with `action: "lint_and_fix"` on source with auto-fixable issues
- **THEN** the tool returns the corrected source code and any remaining non-fixable issues

#### Scenario: List rules returns rule catalog

- **WHEN** the user calls lint with `action: "list_rules"`
- **THEN** the tool returns a list of all available abaplint rule names with their enabled/disabled status and current configuration

### Requirement: Explicit preset selection

The system SHALL let the caller select the rule preset explicitly — CLI `--preset <btp|onpremise>` flag or MCP `systemType` parameter — defaulting to `onpremise`. Automatic detection from the SAP system info endpoint is not implemented; callers targeting a BTP ABAP Environment must pass the `btp` preset explicitly.

#### Scenario: BTP preset uses cloud rules

- **WHEN** the caller passes `--preset btp` (or `systemType: "btp"`)
- **THEN** the linter enables `cloud_types` and `strict_sql` at Error severity

#### Scenario: Default preset is on-premise

- **WHEN** no preset is provided
- **THEN** the `cloud_types` rule is disabled (on-premise ruleset)

### Requirement: Custom abaplint config override

The system SHALL accept an optional path to a custom `abaplint.jsonc` configuration file (CLI `--config` flag) or an inline rule-override object (MCP tool parameter) that takes precedence over the selected preset.

#### Scenario: Custom config overrides preset

- **WHEN** the user provides a custom abaplint.jsonc config
- **THEN** that config's rules are applied instead of the selected preset

### Requirement: Pre-write lint gate in update_source

The system SHALL provide an opt-in lint gate for `update_source` / `adt source write`. When enabled (`lintBeforeWrite: true` / `--lint-before-write`), diagnostics with keys `parser_error`, `cloud_types`, or `strict_sql` SHALL block the write and report the blocking diagnostics without modifying SAP (`strict_sql` violations block only when the BTP preset enables the rule).

#### Scenario: Gate blocks write on parser error

- **WHEN** `lintBeforeWrite` is enabled and the source has ABAP parser errors
- **THEN** the write is rejected with `isError: true` and the blocking diagnostics are reported (formatted as `rule: message` text; a structured diagnostics payload is tracked as follow-up work)

#### Scenario: Gate allows write when source is clean

- **WHEN** `lintBeforeWrite` is enabled and the source passes lint
- **THEN** the write proceeds normally

#### Scenario: Gate is disabled by default

- **WHEN** no `lintBeforeWrite` flag is set
- **THEN** `update_source` writes without running lint

### Requirement: CLI lint command

The system SHALL expose `adt lint <file>` (or `adt lint --source <text>`) that reads source from a file path or an inline `--source` argument and prints diagnostics to stdout in human-readable or `--json` format. Reading from standard input is not supported.

#### Scenario: Lint a file and print diagnostics

- **WHEN** the user runs `adt lint path/to/myclass.abap`
- **THEN** diagnostics are printed to stdout with file, line, column, severity, and message

#### Scenario: Lint exits non-zero on errors

- **WHEN** linting finds at least one error-severity issue
- **THEN** the CLI exits with a non-zero exit code
Loading
Loading