# adt-mcp – Agent Guide

This document covers the internal architecture, conventions, and extension patterns for the `@abapify/adt-mcp` package. Read it before adding or modifying tools.

---

## What This Package Is

`adt-mcp` is a stateless [Model Context Protocol](https://modelcontextprotocol.io/) server that bridges MCP clients (AI assistants, IDEs) to SAP ABAP Development Tools (ADT) via the same typed contracts used by `adt-cli`. It is a **thin MCP adapter** — all business logic lives in `@abapify/adt-client`, `@abapify/adt-contracts`, and the domain packages.

---

## Directory Layout

```
packages/adt-mcp/
├── src/
│   ├── bin/
│   │   ├── adt-mcp.ts          # stdio transport entry-point
│   │   └── adt-mcp-http.ts     # Streamable HTTP transport entry-point
│   ├── lib/
│   │   ├── server.ts           # createMcpServer() factory
│   │   ├── types.ts            # ConnectionParams, ToolContext
│   │   ├── http/               # HTTP transport — server, auth, CORS, OIDC,
│   │   │                       # multi-system registry
│   │   ├── session/            # registry.ts, changeset.ts, types.ts —
│   │   │                       # session-scoped state for HTTP transport
│   │   ├── tools/
│   │   │   ├── index.ts        # registerTools() – wires all tools
│   │   │   ├── shared-schemas.ts  # connectionShape (Zod, reused by all tools)
│   │   │   ├── utils.ts        # extractObjectReferences, resolveObjectUriFromType
│   │   │   ├── <tool-name>.ts  # one file per tool
│   │   │   └── ...
│   │   └── mock/
│   │       ├── server.ts       # createMockAdtServer() – in-process HTTP server
│   │       ├── fixtures.ts     # static XML/JSON fixtures
│   │       └── index.ts        # mock public API
└── tests/
    └── integration.test.ts     # integration tests (node:test + mock server)
```

---

## Key Invariants

### 1. One file per tool

Each MCP tool lives in `src/lib/tools/<tool-name>.ts` and exports a single `register<ToolName>Tool(server, ctx)` function. Registration is wired in `tools/index.ts`.

### 2. Schema-based serialisation – no manual XML

Request bodies and response parsing **must** go through the typed schemas in `@abapify/adt-schemas`. The `@abapify/adt-client` adapter calls `schema.build()` / `schema.parse()` automatically when a contract is used.

**Correct:**

```typescript
// body: ObjectReferencesBody (typed against adtcore schema)
await client.adt.activation.activate.post(params, body);
```

**Wrong:**

```typescript
// ❌ manual XML string + third-party parser
const xml = `<adtcore:objectReferences ...>`;
const parsed = new XMLParser().parse(rawXml);
```

If a required endpoint has no contract yet, add one to `@abapify/adt-contracts` first.

### 3. No external XML parser dependency

`fast-xml-parser`, `xml2js`, and similar libraries must not be used. The XSD→schema→contract pipeline handles all XML.

### 4. Two transports, two state models

The server supports **stdio** and **Streamable HTTP**. They share every tool handler but differ in state:

- **stdio** — stateless, connection-per-call. Each tool call constructs a fresh `AdtClient` from the arguments and no state persists across calls. This preserves the original invariant.
- **Streamable HTTP** — session-scoped state. Each MCP session (`Mcp-Session-Id`) owns a cached `AdtClient`, a lock registry, and an optional active changeset. State is cleaned up on session close (explicit `DELETE /mcp`, `sap_disconnect`, or idle TTL). Across sessions and processes no state persists; credentials are never written to disk.

Source of truth for the exact scenarios: [`openspec/changes/add-mcp-http-transport/specs/adt-mcp/spec.md`](../../openspec/changes/add-mcp-http-transport/specs/adt-mcp/spec.md). Do not duplicate the spec here.

### 5. All tools return JSON text

Every tool handler returns:

```typescript
{
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }];
}
```

or on error:

```typescript
{ isError: true, content: [{ type: 'text', text: 'Descriptive error message' }] }
```

### 6. Zod for input validation

All tool inputs are declared with `z.string()`, `z.number()`, etc. The shared `connectionShape` (from `shared-schemas.ts`) is spread into every tool's schema.

---

## Adding a New Tool

### Step 1 – Check if a contract exists

Look in `packages/adt-contracts/src/adt/` for the relevant domain. If the endpoint you need is missing, add a contract first following the existing patterns (see `adt-contracts/AGENTS.md`).

### Step 2 – Create the tool file

```typescript
// src/lib/tools/my-new-tool.ts
import { z } from 'zod';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { ToolContext } from '../types';
import { connectionShape } from './shared-schemas';

export function registerMyNewTool(server: McpServer, ctx: ToolContext): void {
  server.tool(
    'my_new_tool',
    'One-line description shown to the AI client',
    {
      ...connectionShape,
      myParam: z.string().describe('What this parameter does'),
    },
    async (args) => {
      try {
        const client = ctx.getClient(args);
        const result = await client.adt.someNamespace.someEndpoint(
          args.myParam,
        );
        return {
          content: [
            { type: 'text' as const, text: JSON.stringify(result, null, 2) },
          ],
        };
      } catch (error) {
        return {
          isError: true,
          content: [
            {
              type: 'text' as const,
              text: `My new tool failed: ${error instanceof Error ? error.message : String(error)}`,
            },
          ],
        };
      }
    },
  );
}
```

### Step 3 – Register in tools/index.ts

```typescript
import { registerMyNewTool } from './my-new-tool.js';
// ...
export function registerTools(server: McpServer, ctx: ToolContext): void {
  // ... existing tools
  registerMyNewTool(server, ctx);
}
```

### Step 4 – Add mock fixture and integration test

1. Add a fixture response to `src/lib/mock/fixtures.ts`
2. Add a route handler in `src/lib/mock/server.ts` (`matchRoute` function)
3. Add a `describe` block in `tests/integration.test.ts`

### Step 5 – Update README and feature parity table

Update `README.md` with the new tool's parameters and return shape, and update the **Feature Parity Map** table.

---

## Object URI Resolution

Most tools that target a specific object use `resolveObjectUriFromType()` first (zero-network, type-based), then fall back to `quickSearch`:

```typescript
let objectUri = args.objectType
  ? resolveObjectUriFromType(args.objectType, args.objectName)
  : undefined;

if (!objectUri) {
  const searchResult =
    await client.adt.repository.informationsystem.search.quickSearch({
      query: args.objectName,
      maxResults: 10,
    });
  const match = extractObjectReferences(searchResult).find(
    (o) => String(o.name ?? '').toUpperCase() === args.objectName.toUpperCase(),
  );
  if (!match?.uri) return notFoundError(args.objectName);
  objectUri = match.uri;
}
```

`resolveObjectUriFromType` lives in `utils.ts` and covers the most common SAP object types (PROG, CLAS, INTF, FUGR, DEVC, TABL, DOMA, DTEL, TTYP). Extend it if you need to handle new types.

---

## Lock Protocol (update_source)

Source writes follow the SAP ADT security-session lock protocol via `@abapify/adt-locks`:

```
1. createLockService(client).lock(objectUri, { transport })  → lockHandle
2. PUT source to  objectUri/source/main?lockHandle=...
3. createLockService(client).unlock(objectUri, { lockHandle })
```

Always unlock in a `finally`-equivalent catch block to avoid stuck locks. On the HTTP transport the session lock registry acts as the safety net — on session close every still-held lock is released before the SAP security session is destroyed.

---

## Transports

| Entry-point           | Transport        | Selected when                                         |
| --------------------- | ---------------- | ----------------------------------------------------- |
| `bin/adt-mcp.ts`      | stdio (JSON-RPC) | Default.                                              |
| `bin/adt-mcp-http.ts` | Streamable HTTP  | Always runs the HTTP server on `--port` / `MCP_PORT`. |

Legacy MCP-over-SSE is deliberately not supported; the HTTP code path answers `POST /mcp`, `GET /mcp`, and `DELETE /mcp` only.

User-facing deployment documentation: [`docs/deployment/mcp-http.md`](../../docs/deployment/mcp-http.md).

---

## Session registry + changesets (HTTP transport only)

Session-scoped state lives in `src/lib/session/` and is owned by the HTTP server:

- `registry.ts` — maps `Mcp-Session-Id` → cached `AdtClient`, lock registry, optional active changeset. Idle TTL eviction is driven by the `--ttl` flag.
- `changeset.ts` — transactional batch used by the `changeset_*` tools. Pending operations are executed in order on `changeset_commit`; `changeset_rollback` discards them and releases any locks taken while building the batch.
- `types.ts` — shared interfaces.

On session close (`DELETE /mcp`, `sap_disconnect`, or TTL expiry) the server releases every lock, DELETEs the SAP security session, and drops the cached client. Partial failures are logged but do not abort the remaining cleanup steps.

---

## Auth modes (HTTP transport only)

`src/lib/http/auth.ts` implements four mutually-exclusive modes selected from CLI flags / env:

| Mode     | Selected when                                        | Check                                                                                                  |
| -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `none`   | no auth flags                                        | none; logs a warning if the server is not bound to loopback.                                           |
| `bearer` | `--auth-token` or `MCP_AUTH_TOKEN`                   | `Authorization: Bearer <token>` compared with `crypto.timingSafeEqual`.                                |
| `proxy`  | `--trust-forwarded-auth` or `TRUST_FORWARDED_AUTH=1` | Require `x-forwarded-user`. Additional hints collected from `x-forwarded-email`, `x-forwarded-groups`. |
| `oauth`  | `--oauth-issuer` / `OAUTH_ISSUER`                    | OIDC JWT validation via `src/lib/http/oauth.ts` (Okta / Entra ID / Cognito / …).                       |

The spec enumerates the expected behaviour of each mode — keep the implementation and the spec in sync rather than forking documentation here.

---

## Mock Server

`createMockAdtServer()` starts an in-process `http.Server` on a random port. It:

- Returns a fresh random CSRF token per instance (no hardcoded secrets)
- Routes requests via `matchRoute(method, url)` in `server.ts`
- Loads static XML/JSON fixtures from `fixtures.ts`

To add a new endpoint to the mock:

1. Add the fixture string/object to `fixtures.ts`
2. Add an `if` branch in `matchRoute()` in `server.ts`
3. Add the corresponding `describe` + `it` in `integration.test.ts`

---

## Build & Verify

```bash
bunx nx build adt-mcp      # tsdown build
bunx nx typecheck adt-mcp  # tsc --noEmit
bunx nx lint adt-mcp       # ESLint
bunx nx test adt-mcp       # integration tests (node:test)
bunx nx format:write       # Prettier (run before every commit)
```

---

## Dependency Rules

`adt-mcp` may depend on:

| Package                     | Purpose                                     |
| --------------------------- | ------------------------------------------- |
| `@abapify/adt-cli`          | CLI service functions (see Policy below)    |
| `@abapify/adt-client`       | Typed ADT client (contracts + HTTP adapter) |
| `@abapify/adt-contracts`    | ADT endpoint contracts                      |
| `@abapify/adt-schemas`      | Schema types for typed body/response        |
| `@abapify/adt-lint`         | Local ABAP linting + context helper logic   |
| `@abapify/adt-locks`        | Lock/unlock protocol implementation         |
| `@abapify/adt-aunit`        | AUnit domain plugin                         |
| `@abapify/adt-rfc`          | RFC domain plugin                           |
| `@abapify/adt-plugin-*`     | Object-type / format plugins                |
| `@modelcontextprotocol/sdk` | MCP server primitives                       |
| `zod`                       | Input validation                            |

## Dependencies policy: MCP wraps CLI services

**MCP is a thin MCP-transport adapter over the CLI service layer.** This is a
deliberate architectural decision — see the _MCP ↔ CLI Coupling_ section in
the repo-root `AGENTS.md` for the rationale.

Concrete consequences for this package:

- **Do** import service functions from `@abapify/adt-cli`
  (exported via `packages/adt-cli/src/index.ts`) instead of re-implementing
  them. If logic exists in a CLI command, a tool handler here should
  delegate to the same service — never fork it.
- **Do** depend on domain plugin packages (`@abapify/adt-aunit`,
  `@abapify/adt-rfc`, `@abapify/adt-plugin-abapgit`, …) when a tool needs
  their object handlers. These are the same plugins the CLI uses.
- **Do not** depend on `@abapify/adt-cli`'s command/commander code —
  commands are a transport layer and must not be imported from MCP. Depend
  on _services_, not _commands_.
- **Do not** create a dependency from `adt-cli` back to `adt-mcp` — that
  would introduce a cycle. The arrow always points `adt-mcp → adt-cli`.

The parity test harness at
`packages/adt-cli/tests/e2e/parity.*.test.ts` is the source of truth for
CLI ↔ MCP feature parity. Any tool change that isn't exercised by a parity
test is considered incomplete.

It must **not** depend on:

- `fast-xml-parser`, `xml2js`, or any other XML parser (use contracts/schemas)
- Commander/CLI-transport modules from `@abapify/adt-cli` (import services
  only)
