fix(roadtrip): gate the via tools on the addon, the way the routes are

The controller carries @RequireAddon(ROADTRIP), the four MCP tools carried
nothing. On an instance that left the addon off, REST answered 404 while
list_route_vias, add_route_via, reanchor_route_vias and remove_route_via kept
reading and writing, which is exactly the parity the repo refuses to break.

Same shape as budget: an addonGate constant, AddonsService held publicly so
the module-level gate can reach it, AddonsModule in the imports.
This commit is contained in:
Maurice
2026-09-04 19:21:11 +02:00
committed by Maurice
parent 4f57c1ee13
commit f1fa155899
2 changed files with 20 additions and 2 deletions
+16
View File
@@ -5,6 +5,17 @@ import { DatabaseService } from '../database/database.service';
import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service';
import { demoDenied, noAccess, permissionDenied } from '../../mcp/tools/_shared';
import { AuthService } from '../auth/auth.service';
import { ADDON_IDS } from '../../addons';
import { addonGate } from '../addons/addon-gate';
import { AddonsService } from '../addons/addons.service';
/**
* The whole surface rides the road trip addon, the same way the controller does
* (`@RequireAddon`). Without the gate the REST route answers 404 on an instance that
* left the addon off while these four tools kept reading and writing vias, which is
* exactly the parity the repo forbids breaking.
*/
const roadtripAddonOn = addonGate(ADDON_IDS.ROADTRIP);
/**
* Road-trip via points over MCP, the same surface the REST routes expose (#1797).
@@ -21,6 +32,7 @@ export class RoadtripMcp {
private readonly db: DatabaseService,
private readonly guards: McpToolGuardsService,
private readonly auth: AuthService,
readonly addons: AddonsService,
) {}
@Tool({
@@ -32,6 +44,7 @@ export class RoadtripMcp {
},
annotations: TOOL_ANNOTATIONS_READONLY,
access: { group: 'trips', mode: 'read' },
when: roadtripAddonOn,
})
async listVias({ tripId, dayId }: { tripId: number; dayId?: number }, ctx: McpContext) {
if (!this.db.canAccessTrip(tripId, ctx.userId)) return noAccess();
@@ -54,6 +67,7 @@ export class RoadtripMcp {
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
access: { group: 'trips', mode: 'write' },
when: roadtripAddonOn,
})
async addVia(
{ tripId, dayId, after_order_index, lat, lng }: { tripId: number; dayId: number; after_order_index: number; lat: number; lng: number },
@@ -81,6 +95,7 @@ export class RoadtripMcp {
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
access: { group: 'trips', mode: 'write' },
when: roadtripAddonOn,
})
async reanchorVias(
{ tripId, dayId, vias, remove }: { tripId: number; dayId: number; vias: { id: number; after_order_index: number }[]; remove?: number[] },
@@ -103,6 +118,7 @@ export class RoadtripMcp {
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
access: { group: 'trips', mode: 'write' },
when: roadtripAddonOn,
})
async removeVia({ tripId, dayId, viaId }: { tripId: number; dayId: number; viaId: number }, ctx: McpContext) {
if (this.auth.isDemoUser(ctx.userId)) return demoDenied();
+4 -2
View File
@@ -5,11 +5,13 @@ import { RoadtripMcp } from './roadtrip.mcp';
import { McpSharedModule } from '../mcp-shared/mcp-shared.module';
import { PermissionsModule } from '../permissions/permissions.module';
import { AuthModule } from '../auth/auth.module';
import { AddonsModule } from '../addons/addons.module';
/** Road trip domain (#1797): the points a drive is routed through. Registered in AppModule. */
@Module({
// McpShared brings the tool guards, Auth the demo check, Permissions the trip guard.
imports: [McpSharedModule, PermissionsModule, AuthModule],
// McpShared brings the tool guards, Auth the demo check, Permissions the trip guard,
// Addons the enabled-check the MCP tools gate on (the controller has @RequireAddon).
imports: [McpSharedModule, PermissionsModule, AuthModule, AddonsModule],
controllers: [RoadtripController],
providers: [RoadtripService, RoadtripMcp],
exports: [RoadtripService],