From e2a7c1462e3c0994051860864eef38699514abaf Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:34:28 -0300 Subject: [PATCH 01/11] docs: define MCP application lifecycle rules --- ...-mcp-application-lifecycle-rules-design.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-28-mcp-application-lifecycle-rules-design.md diff --git a/docs/superpowers/specs/2026-07-28-mcp-application-lifecycle-rules-design.md b/docs/superpowers/specs/2026-07-28-mcp-application-lifecycle-rules-design.md new file mode 100644 index 0000000..2d18456 --- /dev/null +++ b/docs/superpowers/specs/2026-07-28-mcp-application-lifecycle-rules-design.md @@ -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. From 4327549037b89a7886680a37a1fdf8d347bafe2c Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:35:16 -0300 Subject: [PATCH 02/11] docs: add MCP lifecycle implementation plan --- ...6-07-28-mcp-application-lifecycle-rules.md | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-28-mcp-application-lifecycle-rules.md diff --git a/docs/superpowers/plans/2026-07-28-mcp-application-lifecycle-rules.md b/docs/superpowers/plans/2026-07-28-mcp-application-lifecycle-rules.md new file mode 100644 index 0000000..0ae8dc0 --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-mcp-application-lifecycle-rules.md @@ -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. From bcb12ffe3d5c79b12cf7f4e08d310f91b74f47c9 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:36:29 -0300 Subject: [PATCH 03/11] test(mcp): define application lifecycle safeguards --- ...ApplicationLifecycleRulesResourceTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/test/java/com/jobtracker/unit/mcp/McpApplicationLifecycleRulesResourceTest.java diff --git a/src/test/java/com/jobtracker/unit/mcp/McpApplicationLifecycleRulesResourceTest.java b/src/test/java/com/jobtracker/unit/mcp/McpApplicationLifecycleRulesResourceTest.java new file mode 100644 index 0000000..b9dc65f --- /dev/null +++ b/src/test/java/com/jobtracker/unit/mcp/McpApplicationLifecycleRulesResourceTest.java @@ -0,0 +1,28 @@ +package com.jobtracker.unit.mcp; + +import com.jobtracker.mcp.resources.McpApplicationLifecycleRulesResource; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class McpApplicationLifecycleRulesResourceTest { + + private final McpApplicationLifecycleRulesResource resource = + new McpApplicationLifecycleRulesResource(); + + @Test + void applicationLifecycleRules_separateStatusFromArchiveAndRequireLeastDestructiveMutation() { + String text = resource.applicationLifecycleRules(null); + + assertThat(text) + .contains("different concepts") + .contains("Rejected") + .contains("MUST NOT archive") + .contains("dar baixa") + .contains("soft-delete") + .contains("least destructive mutation") + .contains("Restore-Application") + .contains("permanent deletion") + .contains("Do not delete and recreate"); + } +} From 0fcd998b712cd41340d847720ef7ff072ec7defc Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:37:18 -0300 Subject: [PATCH 04/11] test(mcp): cover lifecycle mutation descriptions --- .../unit/mcp/McpApplicationToolsTest.java | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java b/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java index 2498af1..1206f5f 100644 --- a/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java +++ b/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java @@ -113,12 +113,7 @@ void getOverdueApplications_delegatesToService() { @Test void createApplicationTool_descriptionMandatesAutomaticRegistration() { - Method method = java.util.Arrays.stream(McpApplicationTools.class.getDeclaredMethods()) - .filter(m -> m.getName().equals("createApplication")) - .findFirst() - .orElseThrow(); - - String description = method.getAnnotation(McpTool.class).description(); + String description = toolDescription("createApplication"); assertThat(description) .contains("must be called automatically") @@ -193,6 +188,14 @@ void updateApplicationStatus_buildsCorrectRequest() { assertThat(captor.getValue().status()).isEqualTo("Teste Técnico"); } + @Test + void updateApplicationStatusTool_descriptionSeparatesStatusFromArchival() { + assertThat(toolDescription("updateApplicationStatus")) + .contains("does not archive") + .contains("Rejected") + .contains("status only"); + } + @Test void updateApplicationReminder_passesEnabledFlag() { UUID id = UUID.randomUUID(); @@ -227,6 +230,26 @@ void archiveApplication_delegatesToService() { verify(applicationService).archive(id); } + @Test + void archiveApplicationTool_descriptionRequiresExplicitArchiveIntent() { + assertThat(toolDescription("archiveApplication")) + .contains("soft-delete") + .contains("Never infer") + .contains("Rejected") + .contains("Approved") + .contains("explicit"); + } + + @Test + void restoreApplication_delegatesToService() { + UUID id = UUID.randomUUID(); + when(applicationService.restore(id)).thenReturn(applicationResponseWithId(id)); + + tools.restoreApplication(null, id.toString()); + + verify(applicationService).restore(id); + } + @Test void deleteApplication_delegatesToService() { UUID id = UUID.randomUUID(); @@ -236,6 +259,21 @@ void deleteApplication_delegatesToService() { verify(applicationService).delete(id); } + @Test + void deleteApplicationTool_descriptionRequiresExplicitPermanentDeletion() { + assertThat(toolDescription("deleteApplication")) + .contains("permanently") + .contains("explicitly requests permanent deletion"); + } + + private static String toolDescription(String javaMethodName) { + Method method = java.util.Arrays.stream(McpApplicationTools.class.getDeclaredMethods()) + .filter(candidate -> candidate.getName().equals(javaMethodName)) + .findFirst() + .orElseThrow(); + return method.getAnnotation(McpTool.class).description(); + } + private static ApplicationResponse applicationResponseWithId(UUID id) { return new ApplicationResponse( id, null, null, null, null, From 2f0c65ed20896033c44168ee4a001e06596fc2b0 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:38:29 -0300 Subject: [PATCH 05/11] test: define archive restoration behavior --- .../unit/ApplicationServiceTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java b/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java index 07a56e3..c0aee83 100644 --- a/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java +++ b/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java @@ -132,6 +132,25 @@ void updateStatus_shouldUpdateStatus() { verify(gamificationService).onApplicationStatusUpdated(app, "RH"); } + @Test + void updateStatus_shouldNotChangeArchiveState() { + LocalDateTime archivedAt = LocalDateTime.now().minusDays(1); + app.setArchived(true); + app.setArchivedAt(archivedAt); + UpdateStatusRequest statusRequest = new UpdateStatusRequest("Rejected"); + when(securityUtils.getCurrentUserId()).thenReturn(USER_UUID); + when(applicationRepository.findByIdAndUserId(APP_UUID, USER_UUID)).thenReturn(Optional.of(app)); + when(applicationStatusRepository.existsByName("Rejected")).thenReturn(true); + when(applicationRepository.save(app)).thenReturn(app); + when(applicationMapper.toResponse(app)).thenReturn(response); + + applicationService.updateStatus(APP_UUID, statusRequest); + + assertThat(app.getStatus()).isEqualTo("Rejected"); + assertThat(app.isArchived()).isTrue(); + assertThat(app.getArchivedAt()).isEqualTo(archivedAt); + } + @Test void updateStatus_shouldClearApplicationDate_whenMarkedToSendLater() { UpdateStatusRequest statusRequest = new UpdateStatusRequest(null); @@ -211,6 +230,24 @@ void archive_shouldArchiveApplication() { assertThat(app.getArchivedAt()).isNotNull(); } + @Test + void restore_shouldRestoreApplicationWithoutChangingStatus() { + app.setArchived(true); + app.setArchivedAt(LocalDateTime.now().minusDays(1)); + app.setStatus("Rejected"); + when(securityUtils.getCurrentUserId()).thenReturn(USER_UUID); + when(applicationRepository.findByIdAndUserId(APP_UUID, USER_UUID)).thenReturn(Optional.of(app)); + when(applicationRepository.save(app)).thenReturn(app); + when(applicationMapper.toResponse(app)).thenReturn(response); + + applicationService.restore(APP_UUID); + + assertThat(app.isArchived()).isFalse(); + assertThat(app.getArchivedAt()).isNull(); + assertThat(app.getStatus()).isEqualTo("Rejected"); + verify(applicationRepository).save(app); + } + @Test void markDmSent_shouldAwardOnlyOnFirstSend() { when(securityUtils.getCurrentUserId()).thenReturn(USER_UUID); From 9a2451313595f4241f00825a442657a0b7545e93 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:40:05 -0300 Subject: [PATCH 06/11] feat(mcp): register application lifecycle resource --- src/main/java/com/jobtracker/mcp/McpResourcesConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/jobtracker/mcp/McpResourcesConfig.java b/src/main/java/com/jobtracker/mcp/McpResourcesConfig.java index 7723a58..6a5705c 100644 --- a/src/main/java/com/jobtracker/mcp/McpResourcesConfig.java +++ b/src/main/java/com/jobtracker/mcp/McpResourcesConfig.java @@ -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}"; From c6333e09d9748636ff29a974ba252e7643401dbb Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:40:32 -0300 Subject: [PATCH 07/11] feat(mcp): publish application lifecycle rules --- .../McpApplicationLifecycleRulesResource.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/main/java/com/jobtracker/mcp/resources/McpApplicationLifecycleRulesResource.java diff --git a/src/main/java/com/jobtracker/mcp/resources/McpApplicationLifecycleRulesResource.java b/src/main/java/com/jobtracker/mcp/resources/McpApplicationLifecycleRulesResource.java new file mode 100644 index 0000000..396ccae --- /dev/null +++ b/src/main/java/com/jobtracker/mcp/resources/McpApplicationLifecycleRulesResource.java @@ -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. + """; + } +} From 78d6c4cbc8c88ecf34f4b6f6d79c13ae752fcd98 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:41:53 -0300 Subject: [PATCH 08/11] fix(mcp): separate status and archive mutations --- .../mcp/tools/McpApplicationTools.java | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java b/src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java index 1ef8a1b..52e30b7 100644 --- a/src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java +++ b/src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java @@ -212,7 +212,10 @@ public ApplicationResponse updateApplication( @McpTool( name = "Update-Application-Status", title = "Update Application Status", - description = "Update only the status of an existing job application. IMPORTANT: Call List-Statuses first to get valid status values. Never use a status value from memory.", + description = "Update only the status of an existing job application. IMPORTANT: Call List-Statuses first to get valid status values. " + + "Updating a terminal status does not archive the application. Rejection intent, including Rejected, retorno negativo, " + + "não avançou, não passou, empresa recusou, or dar baixa with a rejection message, requires this status only mutation. " + + "Never call Archive-Application unless the user separately requests archival.", annotations = @McpAnnotations( title = "Update Application Status", readOnlyHint = false, @@ -265,7 +268,9 @@ public void markRecruiterDmSent( @McpTool( name = "Archive-Application", title = "Archive Application", - description = "Archive an application so it is hidden from the default active list.", + description = "Soft-delete an application so it is hidden from the default active list while preserving its status and history. " + + "Never infer archive intent from Rejected, Approved, rejection messages, or other terminal statuses. " + + "Use only when the user explicitly asks to archive or clearly states withdrawal, abandonment, duplication, test, invalid, or obsolete-record intent.", annotations = @McpAnnotations( title = "Archive Application", readOnlyHint = false, @@ -279,10 +284,28 @@ public void archiveApplication( applicationService.archive(UUID.fromString(id)); } + @McpTool( + name = "Restore-Application", + title = "Restore Application", + description = "Restore a soft-deleted application to the active list. Preserve its current status and all other data. " + + "Use this tool instead of deleting and recreating a record to change its archive state.", + annotations = @McpAnnotations( + title = "Restore Application", + readOnlyHint = false, + destructiveHint = false, + idempotentHint = true, + openWorldHint = false)) + @AuditMcpOperation(action = "Restore-Application") + public void restoreApplication( + McpSyncRequestContext ctx, + @McpToolParam(required = true, description = "Application UUID") String id) { + applicationService.restore(UUID.fromString(id)); + } + @McpTool( name = "Delete-Application", title = "Delete Application", - description = "Permanently delete an application.", + description = "Permanently delete an application. Use only when the user explicitly requests permanent deletion; otherwise prefer Archive-Application.", annotations = @McpAnnotations( title = "Delete Application", readOnlyHint = false, From 8dcba6a4a7050832cd7e03c7279d490df544cb74 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:43:14 -0300 Subject: [PATCH 09/11] feat(mcp): add application restore operation --- .../com/jobtracker/service/ApplicationService.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/jobtracker/service/ApplicationService.java b/src/main/java/com/jobtracker/service/ApplicationService.java index f7d9bd9..0baf711 100644 --- a/src/main/java/com/jobtracker/service/ApplicationService.java +++ b/src/main/java/com/jobtracker/service/ApplicationService.java @@ -173,6 +173,16 @@ public ApplicationResponse archive(UUID id) { return applicationMapper.toResponse(applicationRepository.save(app)); } + @Transactional + public ApplicationResponse restore(UUID id) { + UUID userId = securityUtils.getCurrentUserId(); + JobApplication app = applicationRepository.findByIdAndUserId(id, userId) + .orElseThrow(() -> new ResourceNotFoundException("Application not found with id: " + id)); + app.setArchived(false); + app.setArchivedAt(null); + return applicationMapper.toResponse(applicationRepository.save(app)); + } + @Transactional(readOnly = true) public ApplicationPageResponse getAll(ApplicationFilter filter, int page, int size, String sort) { UUID userId = securityUtils.getCurrentUserId(); From 264cf9f0565645150d994f3a880390d6bcea1567 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:44:04 -0300 Subject: [PATCH 10/11] docs(mcp): add lifecycle rules to server instructions --- src/main/resources/application.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d7a1332..99e3801 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,6 +19,11 @@ spring: call fails — surface the failure and retry instead. A resume request is complete only once this server returns a Google Doc (documentUrl) and PDF (pdfUrl) linked to the application. See resource://job-apply-tracker/resume-workflow-rules for the full workflow. + Application status and archival are independent concepts. Rejected or Approved status + changes MUST NOT archive an application. Never infer archival from a terminal status or + rejection message; apply the least destructive mutation and archive only from explicit + or clearly established archive intent. See + resource://job-apply-tracker/application-lifecycle-rules for the full lifecycle policy. # Claude.ai (and other modern MCP connectors) speak the Streamable HTTP # transport: a single endpoint that handles both POST (JSON-RPC) and GET # (SSE stream). Requires Spring AI 1.1+. The endpoint defaults to /mcp, From bcd889a322df38992249b3b0e916d3f5b53e89be Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 28 Jul 2026 22:48:26 -0300 Subject: [PATCH 11/11] test(mcp): validate lifecycle wording case-insensitively --- .../java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java b/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java index 1206f5f..25a840a 100644 --- a/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java +++ b/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java @@ -233,7 +233,7 @@ void archiveApplication_delegatesToService() { @Test void archiveApplicationTool_descriptionRequiresExplicitArchiveIntent() { assertThat(toolDescription("archiveApplication")) - .contains("soft-delete") + .containsIgnoringCase("soft-delete") .contains("Never infer") .contains("Rejected") .contains("Approved") @@ -262,7 +262,7 @@ void deleteApplication_delegatesToService() { @Test void deleteApplicationTool_descriptionRequiresExplicitPermanentDeletion() { assertThat(toolDescription("deleteApplication")) - .contains("permanently") + .containsIgnoringCase("permanently") .contains("explicitly requests permanent deletion"); }