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
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