Skip to content
214 changes: 214 additions & 0 deletions docs/superpowers/plans/2026-07-28-mcp-application-lifecycle-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# MCP Application Lifecycle Rules Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Prevent MCP clients from confusing terminal application statuses with archival and add a reversible archive workflow.

**Architecture:** Publish a dedicated lifecycle-rules MCP resource, reference it from the server instructions, reinforce the same constraints in mutation-tool descriptions, and expose restoration as a focused service method and MCP tool. Existing status updates remain independent from archive state.

**Tech Stack:** Java 21, Spring Boot 3.5, Spring AI Community MCP annotations, JUnit 5, AssertJ, Mockito, Maven.

## Global Constraints

- `status` and `archived` represent independent concepts.
- Updating status to `Rejected` or `Approved` must not archive the application.
- Ambiguous archive intent must result in no archive mutation.
- Use the least destructive mutation that satisfies the request.
- Permanent deletion requires explicit user intent.
- Restoring preserves status and all non-archive fields.
- Do not add a REST restore endpoint in this change.

---

### Task 1: Publish lifecycle rules as an MCP resource

**Files:**
- Modify: `src/main/java/com/jobtracker/mcp/McpResourcesConfig.java`
- Create: `src/main/java/com/jobtracker/mcp/resources/McpApplicationLifecycleRulesResource.java`
- Create: `src/test/java/com/jobtracker/unit/mcp/McpApplicationLifecycleRulesResourceTest.java`
- Modify: `src/main/resources/application.yml`

**Interfaces:**
- Produces: `McpResourcesConfig.URI_APPLICATION_LIFECYCLE_RULES`
- Produces: `String McpApplicationLifecycleRulesResource.applicationLifecycleRules(McpSyncServerExchange exchange)`

- [ ] **Step 1: Write the failing resource test**

Create a test that calls `applicationLifecycleRules(null)` and asserts the returned Markdown contains the independence rule, rejection mapping, `dar baixa` example, explicit archive semantics, least-destructive mutation rule, restoration behavior, and permanent-delete safeguard.

- [ ] **Step 2: Run the focused test and verify it fails**

Run:

```bash
mvn -Dtest=McpApplicationLifecycleRulesResourceTest test
```

Expected: compilation failure because `McpApplicationLifecycleRulesResource` does not exist.

- [ ] **Step 3: Add the URI constant and resource implementation**

Add:

```java
public static final String URI_APPLICATION_LIFECYCLE_RULES =
"resource://job-apply-tracker/application-lifecycle-rules";
```

Implement the resource following the existing `McpApplicationCreationRulesResource` pattern with `text/markdown`, assistant audience, priority `1.0d`, and `LAST_MODIFIED = "2026-07-28"`.

- [ ] **Step 4: Extend the global MCP instructions**

Append a concise mandatory lifecycle summary to `spring.ai.mcp.server.instructions` and direct clients to `resource://job-apply-tracker/application-lifecycle-rules` for the full policy.

- [ ] **Step 5: Run the focused test and verify it passes**

```bash
mvn -Dtest=McpApplicationLifecycleRulesResourceTest test
```

Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add src/main/java/com/jobtracker/mcp/McpResourcesConfig.java \
src/main/java/com/jobtracker/mcp/resources/McpApplicationLifecycleRulesResource.java \
src/test/java/com/jobtracker/unit/mcp/McpApplicationLifecycleRulesResourceTest.java \
src/main/resources/application.yml
git commit -m "docs(mcp): define application lifecycle rules"
```

---

### Task 2: Reinforce mutation semantics in MCP tool descriptions

**Files:**
- Modify: `src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java`
- Modify: `src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java`

**Interfaces:**
- Changes metadata only for `Update-Application-Status`, `Archive-Application`, and `Delete-Application`.

- [ ] **Step 1: Write failing annotation-description tests**

Use reflection to assert:

- `Update-Application-Status` says terminal statuses do not archive and rejection intent uses status-only mutation;
- `Archive-Application` describes soft-delete, forbids inference from `Rejected`/`Approved`, and requires explicit or clearly established archive intent;
- `Delete-Application` requires explicit permanent-deletion intent.

- [ ] **Step 2: Run the focused tests and verify they fail**

```bash
mvn -Dtest=McpApplicationToolsTest test
```

Expected: the new description assertions fail against the current short descriptions.

- [ ] **Step 3: Update the tool descriptions**

Keep the wording concise but duplicate the safety-critical rules at tool level so clients remain protected even when server instructions or resources are cached.

- [ ] **Step 4: Run the focused tests and verify they pass**

```bash
mvn -Dtest=McpApplicationToolsTest test
```

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java \
src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java
git commit -m "fix(mcp): separate status updates from archival"
```

---

### Task 3: Add reversible archive support

**Files:**
- Modify: `src/main/java/com/jobtracker/service/ApplicationService.java`
- Modify: `src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java`
- Modify: `src/test/java/com/jobtracker/unit/ApplicationServiceTest.java`
- Modify: `src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java`

**Interfaces:**
- Produces: `ApplicationResponse ApplicationService.restore(UUID id)`
- Produces: `void McpApplicationTools.restoreApplication(McpSyncRequestContext ctx, String id)` named `Restore-Application`

- [ ] **Step 1: Write failing service tests**

Add `restore_shouldRestoreApplicationWithoutChangingStatus` that starts with `archived = true`, a non-null `archivedAt`, and status `Rejected`; calls `restore(APP_UUID)`; then asserts `archived` is false, `archivedAt` is null, and status remains `Rejected`.

Add `updateStatus_shouldNotChangeArchiveState` that starts archived, updates the status, and asserts both archive fields remain unchanged.

- [ ] **Step 2: Write the failing MCP delegation test**

Add `restoreApplication_delegatesToService`, stubbing `applicationService.restore(id)` and verifying delegation.

- [ ] **Step 3: Run focused tests and verify they fail**

```bash
mvn -Dtest=ApplicationServiceTest,McpApplicationToolsTest test
```

Expected: compilation failure because `restore(UUID)` and `restoreApplication(...)` do not exist.

- [ ] **Step 4: Implement `ApplicationService.restore(UUID)`**

Load the application using the current authenticated user's ID, set `archived` to `false`, clear `archivedAt`, save, and map the response. Do not modify status or any other field.

- [ ] **Step 5: Add the `Restore-Application` MCP tool**

Expose a non-destructive, idempotent mutation tool whose description says it restores a soft-deleted record, preserves status, and must not be simulated by delete/recreate.

- [ ] **Step 6: Run focused tests and verify they pass**

```bash
mvn -Dtest=ApplicationServiceTest,McpApplicationToolsTest test
```

Expected: PASS.

- [ ] **Step 7: Commit**

```bash
git add src/main/java/com/jobtracker/service/ApplicationService.java \
src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java \
src/test/java/com/jobtracker/unit/ApplicationServiceTest.java \
src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java
git commit -m "feat(mcp): add application restore tool"
```

---

### Task 4: Verify the complete change

**Files:**
- Review all files changed in Tasks 1-3.

- [ ] **Step 1: Run the full unit test suite**

```bash
mvn test
```

Expected: BUILD SUCCESS with all tests passing.

- [ ] **Step 2: Inspect the final diff**

Confirm the diff contains no automatic archive-on-status behavior, no REST endpoint, no deletion/recreation workaround, and no unrelated refactoring.

- [ ] **Step 3: Open the pull request**

Use title:

```text
fix(mcp): clarify application lifecycle and add restore support
```

The PR body must summarize the status/archive separation, lifecycle resource, tool-level guardrails, restore capability, and test coverage.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# MCP Application Lifecycle Rules Design

## Problem

The MCP currently describes `Archive-Application` only as hiding a record from the default active list. That wording does not explain the business boundary between the hiring-process status and the record's visibility, so an assistant can incorrectly infer that a terminal status such as `Rejected` should also archive the application.

## Goal

Make the lifecycle semantics explicit and enforce the least-destructive mutation rule: rejection or approval changes only the application status, while archive remains an independent soft-delete operation performed only from explicit or clearly established user intent.

## Design

### 1. Dedicated lifecycle resource

Add `resource://job-apply-tracker/application-lifecycle-rules` as the authoritative Markdown reference for application status, archival, restoration, deletion, ambiguity handling, and examples.

The resource must state that:

- status and `archived` are independent concepts;
- `Rejected` and `Approved` never imply archival;
- rejection phrases, including `dar baixa` when accompanied by a rejection message, map to `Update-Application-Status` with `Rejected` and keep `archived = false`;
- archival is a soft-delete for withdrawal, abandoned/incompatible vacancies, duplicates, tests, invalid or obsolete records, or an explicit archive request;
- ambiguous archival intent must not cause archival;
- the assistant must apply the least destructive mutation;
- permanent deletion requires explicit user intent;
- archive-state changes must not be implemented by deleting and recreating the record.

The MCP server instructions will contain a concise mandatory summary and point clients to this resource.

### 2. Tool-level guardrails

Strengthen `Update-Application-Status`, `Archive-Application`, and `Delete-Application` descriptions so the rules remain visible even when a client caches or omits server-level instructions/resources.

`Update-Application-Status` must explicitly say that terminal status updates do not archive. `Archive-Application` must describe soft-delete semantics and forbid inference from rejection or approval. `Delete-Application` must require explicit permanent-deletion intent.

### 3. Restore capability

Add a dedicated `Restore-Application` MCP tool backed by `ApplicationService.restore(UUID)`. Restoring sets `archived = false` and clears `archivedAt`, preserving the current status and all other application data.

A separate tool is preferred over adding `archived` to `Update-Application` because it keeps full-record updates independent from visibility mutations and avoids accidental archive-state changes when assistants send replacement payloads.

### 4. Tests

Add focused tests for:

- lifecycle resource wording and examples;
- tool descriptions containing the mandatory safeguards;
- `Restore-Application` delegation;
- service restoration clearing both archive fields while preserving the current status;
- existing status updates leaving archive state unchanged.

## Non-goals

- Changing the meaning or stored values of existing statuses.
- Automatically archiving or restoring based on status transitions.
- Adding a REST endpoint for restore in this change.
- Changing default application-list filtering.
1 change: 1 addition & 0 deletions src/main/java/com/jobtracker/mcp/McpResourcesConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class McpResourcesConfig {
public static final String URI_BASE_RESUMES = "resource://job-apply-tracker/base-resumes";
public static final String URI_CURRENT_USER = "resource://job-apply-tracker/current-user";
public static final String URI_APPLICATION_CREATION_RULES = "resource://job-apply-tracker/application-creation-rules";
public static final String URI_APPLICATION_LIFECYCLE_RULES = "resource://job-apply-tracker/application-lifecycle-rules";
public static final String URI_RESUME_WORKFLOW_RULES = "resource://job-apply-tracker/resume-workflow-rules";
public static final String URI_APPLICATION_STATUSES = "resource://job-apply-tracker/application-statuses";
public static final String URI_BASE_RESUME_CONTENT = "resource://job-apply-tracker/base-resume/{resumeId}";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.jobtracker.mcp.resources;

import com.jobtracker.mcp.McpResourcesConfig;
import com.jobtracker.mcp.audit.AuditMcpOperation;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema.Role;
import org.springaicommunity.mcp.annotation.McpResource;
import org.springaicommunity.mcp.annotation.McpResource.McpAnnotations;
import org.springframework.stereotype.Service;

@Service
public class McpApplicationLifecycleRulesResource {

private static final String LAST_MODIFIED = "2026-07-28";

@McpResource(
uri = McpResourcesConfig.URI_APPLICATION_LIFECYCLE_RULES,
name = "Application Lifecycle Rules",
title = "Application Lifecycle Rules",
description = "Mandatory rules separating application status, archival, restoration, and deletion.",
mimeType = "text/markdown",
annotations = @McpAnnotations(
audience = {Role.ASSISTANT},
lastModified = LAST_MODIFIED,
priority = 1.0d))
@AuditMcpOperation(action = "Application Lifecycle Rules")
public String applicationLifecycleRules(McpSyncServerExchange exchange) {
return """
# Job application lifecycle rules

The application status and the `archived` flag represent different concepts and MUST be handled independently.

## Status semantics

- `Rejected`: the company or recruiter rejected the candidate or decided not to continue the hiring process.
- `Approved`: the candidate was approved.
- Other statuses represent the current stage of an active hiring process.
- Changing an application to `Rejected` or `Approved` MUST NOT archive it.

When the user says `rejected`, `retorno negativo`, `não avançou`, `não passou`, or `empresa recusou`:

1. Call `Update-Application-Status` with `Rejected` after obtaining the valid status value from `List-Statuses`.
2. Keep `archived = false`.
3. Do not call `Archive-Application` unless the user separately and explicitly requests archival.

When the user says `dar baixa` while providing or showing a rejection message, interpret it as a status-only
update to `Rejected`, not as an archive request.

## Archive semantics

Archiving is a soft-delete operation used only when the candidate no longer intends to proceed with the
application record, for example:

- the candidate decided not to apply;
- the candidate withdrew from the process;
- the vacancy is incompatible and the candidate abandoned the application;
- the record is a duplicate, test, invalid, or obsolete entry;
- the user explicitly asks to archive the application.

An archived application is hidden from the normal active list but preserved for historical purposes.
Preserve its current status when archiving.

## Mandatory behavior

- Never infer that a terminal status requires archiving.
- Never call `Archive-Application` after setting `Rejected` or `Approved` unless the user explicitly requested archival.
- When archival intent is ambiguous, do not archive. Ask for clarification when clarification is necessary.
- Always apply the least destructive mutation necessary to fulfill the request.
- Use `Restore-Application` to make an archived record active again. Restoration preserves the current status and all other data.
- Permanent deletion is allowed only when the user explicitly requests permanent deletion.
- Do not delete and recreate an application to change its archive state without explicit user approval.

## Examples

User: `A empresa não avançou, dá baixa.`
Action: set status to `Rejected`. Do not archive.

User: `Desisti dessa vaga, pode arquivar.`
Action: archive the application. Preserve its current status.

User: `Essa candidatura foi criada por engano.`
Action: archive it. Delete it only if the user explicitly requests permanent deletion.

User: `Recebi uma rejeição.`
Action: set status to `Rejected`. Keep the record visible and non-archived.

User: `Arquivei por engano, restaure.`
Action: call `Restore-Application`. Preserve the current status.
""";
}
}
Loading
Loading