Skip to main content

rpfm_server/
server_mcp.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! [Model Context Protocol][mcp] server exposed at the `/mcp` endpoint.
12//!
13//! Exposes the methods of the server's API as MCP **tools**, plus a handful
14//! of MCP **resources** (game lists, enum dumps, reference docs) and
15//! **prompts** for common workflows ("open and inspect a pack", "edit a DB
16//! table", "manage dependencies", …). All MCP clients share one [`Session`],
17//! separate from the ones of the WebSocket clients.
18//!
19//! Each tool call sends its method's request through the session, like a
20//! WebSocket client would, and returns the response as structured content,
21//! or as a tool error if it failed. Methods that run as jobs return an MCP
22//! task to clients supporting tasks. For other clients, they're waited for a
23//! while, and return the state of their job.
24//!
25//! The request structs of the methods are the schema of every tool. Their
26//! `JsonSchema` derive is what `rmcp` ships to clients to advertise tool
27//! arguments, so docstrings on individual fields show up directly in MCP
28//! tool listings.
29//!
30//! [mcp]: https://modelcontextprotocol.io/
31//! [`Session`]: crate::session::Session
32//! [`CallToolResult`]: rmcp::model::CallToolResult
33
34use rmcp::ErrorData as McpError;
35use rmcp::handler::server::{common::schema_for_output, prompt::PromptContext, router::prompt::PromptRouter, tool::{IntoCallToolResult, ToolCallContext, ToolRouter}, wrapper::Parameters};
36use rmcp::model::{
37    CacheScope, CallToolRequestParams, CallToolResponse, CallToolResult, CancelTaskParams,
38    ContentBlock, CreateTaskResult, DetailedTask, DiscoverResult, ErrorCode, GetPromptRequestParams, GetPromptResponse, GetTaskParams, GetTaskResult, Implementation,
39    JsonObject, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, ListToolsResult, PaginatedRequestParams, ProgressNotificationParam, PromptMessage,
40    ReadResourceRequestParams, ReadResourceResult, ReadResourceResponse,
41    Resource, ResourceContents, Role, ServerCapabilities, ServerInfo, Task, TaskPayload, TaskStatus,
42};
43use rmcp::service::RequestContext;
44use rmcp::task_manager::DEFAULT_POLL_INTERVAL_MS;
45use rmcp::{prompt, prompt_router, tool, tool_handler, tool_router, RoleServer};
46use time::OffsetDateTime;
47use tokio::sync::broadcast::error::RecvError;
48use time::format_description::well_known::Rfc3339;
49
50use std::sync::Arc;
51use std::time::{Duration, SystemTime};
52
53
54use rpfm_ipc::api::{ApiError, Done, Request, RpcError, RpcOutcome, RpcRequest, RpcResponse};
55use rpfm_ipc::api::diagnostics::{DiagnosticList, IgnoreDiagnostics, ListDiagnostics, RunDiagnostics};
56use rpfm_ipc::api::search::{ListSearchMatches, ReplaceSearchMatches, RunSearch, SearchMatchList, SearchReplaced};
57use rpfm_ipc::api::references::{FindDefinition, FindLoc, FindUsages, GetLocSource, GetReferenceValues, GetRowLocs, LocSourceLookup, ReferenceValues, RowLocation, RowLocs, Usages};
58use rpfm_ipc::api::notes::{AddNote, DeleteNote, ListNotes, NoteEntry, NoteList};
59use rpfm_ipc::api::schema::{
60    DeleteDefinition, GetRawDefinitions, GetMissingDefinitions, GetReferencingColumns, GetTablePatches, ImportPatches, ListSchemaTables, PatchColumn, RawDefinitions, ReferencingColumns, MissingDefinitions, RemovePatches, TablePatches,
61    SchemaTables, SetDefinition, UpdateSchemaFromAssemblyKit, UpdateSchemas,
62};
63use rpfm_ipc::api::translations::{GenerateVanillaTexts, ListTranslations, Translations, VanillaTextsAvailable};
64use rpfm_ipc::api::tools::{
65    AnimsBySkeleton, CompareStartposTables, ExportGltf, ExtractStartposTables, FilePaths, FilesChanged, KeepStartposCampaign, FinishStartpos, GenerateMissingLocs, GenerateStartpos, StartposComparison, StartposValidation, ValidateStartpos, GetOptimizerOptions, GetStartposCampaigns, InitMyMod, LiveExport,
66    MyModCreated, OptimizePack, OptimizerOptionValues, PackMap, PatchSiegeAi, RunLuaTests, SetVideoFormat, SiegeAiPatched, StartStartpos, StartposCampaigns,
67    UpdateAnimIds,
68};
69use rpfm_ipc::api::updates::{ApplyUpdate, CheckUpdate, UpdateStatus};
70use rpfm_ipc::api::files::{
71    AddToAnimPack, DeleteFromAnimPack, ExtractFromAnimPack, FileContents, ListAnimPack, ReadFile, WriteFile,
72    AddFilesFromDisk, CopyFiles, CreateFile, DeleteFiles, DuplicateFiles, ExtractFiles, FileEntry, FileList, FilesAdded, FilesDeleted,
73    FilesExtracted, FilesRenamed, ListFiles, RenameFiles,
74};
75use rpfm_ipc::api::packs::{ClosePack, CloseAllPacks, GetPackInfo, GetPackSettings, NewPack, OpenPack, OpenVanillaPacks, PackDetails, PackSettingsValues, PackSummary, SavePack, UpdatePack, UpdatePackSettings};
76use rpfm_ipc::api::jobs::{CancelJob, GetJobStatus, JobStarted, JobState, JobStatus, WaitForJob};
77use rpfm_ipc::api::session::{DependencyTables, GenerateDependenciesCache, GetSessionStatus, ListDependencyTables, RebuildDependencies, SessionStatus, SetGame};
78use rpfm_ipc::api::tables::{
79    AddKeyDeletes, ColumnValues, EditTable, ExportTsv, FilesEdited, GetColumnValues, GetTableDefinition, GetTableInfo, GetTableRows, GetUnusedNumbers, ImportTsv, MergeTables,
80    RenameKey, TableDefinition, TableEdited, TableInfo, TableRows, TablesMerged, TableUpgraded, UnusedNumbers, UpgradeTable,
81};
82use rpfm_telemetry::sentry;
83
84use crate::jobs::{JobRecord, JobRegistry};
85use crate::session::{Session, SessionId, SessionManager};
86
87//-------------------------------------------------------------------------------//
88//                              Helpers
89//-------------------------------------------------------------------------------//
90
91/// How long tools that run as jobs wait for them before returning their state.
92///
93/// Kept under the usual timeout of MCP clients. Jobs still running after it can be waited for with `wait_for_job`.
94const MCP_JOB_WAIT: Duration = Duration::from_secs(45);
95
96/// How long clients may cache the server's discovery info, its lists of tools, prompts and resources, and its resources.
97///
98/// They only change with the server's version.
99const MCP_CACHE_TTL_MS: u64 = 60 * 60 * 1000;
100
101/// Starts the Sentry transaction of a tool call, following the MCP tracing spec.
102fn start_tool_transaction(tool_name: &str) -> sentry::Transaction {
103    let tx_ctx = sentry::TransactionContext::new(&format!("tools/call {}", tool_name), "mcp.server");
104    let tx = sentry::start_transaction(tx_ctx);
105    tx.set_data("mcp.method.name", sentry::protocol::Value::from("tools/call"));
106    tx.set_data("mcp.tool.name", sentry::protocol::Value::from(tool_name));
107    tx.set_data("mcp.transport", sentry::protocol::Value::from("streamable-http"));
108
109    sentry::configure_scope(|scope| scope.set_span(Some(tx.clone().into())));
110    tx
111}
112
113/// Sends a request of the API to a session and waits for its outcome.
114///
115/// # Errors
116///
117/// Fails if the request can't be serialized.
118async fn send_request<R: Request>(session: &Session, request: &R) -> Result<RpcOutcome, McpError> {
119    let rpc_request = RpcRequest::new(0, request).map_err(|error| McpError {
120        code: ErrorCode::INTERNAL_ERROR,
121        message: format!("Failed to serialize request: {error}").into(),
122        data: None,
123    })?;
124
125    let response = session.call(rpc_request).recv().await
126        .unwrap_or_else(|| RpcResponse::new(0, Err(ApiError::Internal("Session response channel closed unexpectedly".to_owned()))));
127    Ok(response.outcome)
128}
129
130/// Returns an API error as a tool error.
131fn error_result(error: &RpcError) -> CallToolResult {
132    CallToolResult::error(vec![ContentBlock::text(serde_json::to_string(error).unwrap_or_else(|_| error.message.clone()))])
133}
134
135/// Start of the text of the tool errors rmcp returns for params it can't parse.
136const PARAMS_ERROR_PREFIX: &str = "failed to deserialize parameters: ";
137
138/// Returns the state of a job as structured content, or its error as a tool error, like any other tool, if the job failed.
139fn job_status_result(status: &JobStatus) -> CallToolResult {
140    if let JobState::Failed { error } = &status.state {
141        return error_result(error);
142    }
143
144    match serde_json::to_value(status) {
145        Ok(value) => CallToolResult::structured(value),
146        Err(error) => error_result(&ApiError::Internal(error.to_string()).into()),
147    }
148}
149
150/// Waits for a job like [`JobRegistry::wait`], sending the client a progress notification on each change of the job, if it asked for them.
151///
152/// Progress must always increase, but jobs report it from 0 to 100 on each of their stages, so each new stage adds 100 to it.
153async fn wait_reporting_progress(jobs: &JobRegistry, job: u64, context: &RequestContext<RoleServer>) -> Option<JobStatus> {
154    let Some(progress_token) = context.meta.get_progress_token() else {
155        return jobs.wait(job, MCP_JOB_WAIT).await;
156    };
157
158    let mut updates = jobs.subscribe();
159    let wait = jobs.wait(job, MCP_JOB_WAIT);
160    tokio::pin!(wait);
161
162    let mut current_stage = None;
163    let mut stage_offset = 0.0;
164    let mut last_progress = None;
165    loop {
166        tokio::select! {
167            status = &mut wait => return status,
168            update = updates.recv() => match update {
169                Ok(JobStatus { job: updated_job, state: JobState::Running { stage, progress }, .. }) if updated_job == job => {
170                    if stage != current_stage {
171                        stage_offset += 100.0;
172                        current_stage = stage;
173                    }
174
175                    let progress = stage_offset + f64::from(progress.unwrap_or_default());
176                    if last_progress.is_some_and(|last_progress| progress <= last_progress) {
177                        continue;
178                    }
179
180                    last_progress = Some(progress);
181                    let notification = ProgressNotificationParam::new(progress_token.clone(), progress);
182                    let notification = match &current_stage {
183                        Some(stage) => notification.with_message(stage),
184                        None => notification,
185                    };
186
187                    // The client may be gone. The job continues anyway, so there's nothing to do about it.
188                    let _ = context.peer.notify_progress(notification).await;
189                },
190                Ok(_) | Err(RecvError::Lagged(_)) => {},
191                Err(RecvError::Closed) => return wait.await,
192            },
193        }
194    }
195}
196
197/// Returns the ID of the task of a job, unique across MCP sessions so a task of a removed session isn't mistaken for a job of the new one.
198fn task_id(session: SessionId, job: u64) -> String {
199    format!("{session}-{job}")
200}
201
202/// Returns the session and job of a task ID, or `None` if it's not one.
203fn parse_task_id(task_id: &str) -> Option<(SessionId, u64)> {
204    let (session, job) = task_id.split_once('-')?;
205    Some((session.parse().ok()?, job.parse().ok()?))
206}
207
208/// Returns the error of task IDs that aren't of a job of the MCP session.
209fn unknown_task(task_id: &str) -> McpError {
210    McpError::invalid_params(format!("Unknown task: {task_id}"), None)
211}
212
213/// Returns a time as an ISO 8601 timestamp, as tasks need.
214fn timestamp(time: SystemTime) -> String {
215    OffsetDateTime::from(time).format(&Rfc3339).unwrap_or_default()
216}
217
218/// Returns the task of a job of a session.
219///
220/// Ended jobs complete the task with the same result their tool returns when it waits for them, so failed jobs are tool errors.
221fn job_task(session: SessionId, record: &JobRecord) -> DetailedTask {
222    let task = Task::new(task_id(session, record.status.job), TaskStatus::Working, timestamp(record.created_at), timestamp(record.updated_at))
223        .with_poll_interval_ms(DEFAULT_POLL_INTERVAL_MS);
224
225    match &record.status.state {
226        JobState::Queued => DetailedTask::new(task.with_status_message("Queued"), TaskPayload::Working),
227        JobState::Running { stage: Some(stage), .. } => DetailedTask::new(task.with_status_message(stage), TaskPayload::Working),
228        JobState::Running { stage: None, .. } => DetailedTask::new(task, TaskPayload::Working),
229        JobState::Finished { .. } | JobState::Failed { .. } => {
230            let result = match serde_json::to_value(job_status_result(&record.status)) {
231                Ok(serde_json::Value::Object(result)) => result,
232                _ => JsonObject::new(),
233            };
234
235            DetailedTask::new(task, TaskPayload::Completed { result })
236        },
237        JobState::Cancelled => DetailedTask::new(task, TaskPayload::Cancelled),
238    }
239}
240
241/// Build a `Resource` with common fields set.
242fn resource(uri: &str, name: &str, description: &str, mime_type: &str) -> Resource {
243    Resource::new(uri, name)
244        .with_description(description)
245        .with_mime_type(mime_type)
246}
247
248//-------------------------------------------------------------------------------//
249//                              Enums & Structs
250//-------------------------------------------------------------------------------//
251
252/// MCP server running every tool call on the [`Session`] shared by all MCP clients.
253///
254/// One instance is constructed per MCP connection or request by the
255/// `StreamableHttpService` factory wired in `main.rs`. The
256/// `tool_router` and `prompt_router` fields are built once at construction
257/// time from the `#[tool_router]` / `#[prompt_router]` attribute macros
258/// applied further down in this module.
259///
260/// Cheap to clone — only `Arc` and small router structs.
261#[derive(Clone)]
262pub struct McpServer {
263    /// The session manager, to get the session shared by all MCP clients on each call.
264    sessions: Arc<SessionManager>,
265    /// The router auto-generated from `#[tool_router]` annotations.
266    tool_router: ToolRouter<Self>,
267    /// The router auto-generated from `#[prompt_router]` annotations.
268    prompt_router: PromptRouter<Self>,
269}
270
271/// Response of the tools running as jobs: a task, or the state of the job.
272///
273/// rmcp can't turn a [`CallToolResponse`] into a tool response, and the orphan rule forbids implementing
274/// its trait for it here, so this wraps it.
275pub struct JobToolResponse(CallToolResponse);
276
277//-------------------------------------------------------------------------------//
278//                             Implementations
279//-------------------------------------------------------------------------------//
280
281impl IntoCallToolResult for JobToolResponse {
282    fn into_call_tool_result(self) -> Result<CallToolResponse, McpError> {
283        Ok(self.0)
284    }
285}
286
287#[tool_handler(router = self.tool_router)]
288impl rmcp::ServerHandler for McpServer {
289
290    async fn discover(&self, _context: RequestContext<RoleServer>) -> Result<DiscoverResult, McpError> {
291        Ok(DiscoverResult::from_server_info(self.supported_protocol_versions().into_owned(), self.get_info())
292            .with_ttl_ms(MCP_CACHE_TTL_MS)
293            .with_cache_scope(CacheScope::Public))
294    }
295
296    async fn list_tools(&self, _request: Option<PaginatedRequestParams>, _context: RequestContext<RoleServer>) -> Result<ListToolsResult, McpError> {
297        Ok(ListToolsResult::with_all_items(self.tool_router.list_all())
298            .with_ttl_ms(MCP_CACHE_TTL_MS)
299            .with_cache_scope(CacheScope::Public))
300    }
301
302    /// Calls a tool, reporting params rmcp can't parse like the API's own invalid params, instead of as plain text.
303    ///
304    /// Stops waiting for the tool if the client cancels the call, as rmcp drops its response. The request keeps running on the session.
305    async fn call_tool(&self, request: CallToolRequestParams, context: RequestContext<RoleServer>) -> Result<CallToolResponse, McpError> {
306        let cancellation = context.ct.clone();
307        let response = tokio::select! {
308            response = self.tool_router.call(ToolCallContext::new(self, request, context)) => response?,
309            _ = cancellation.cancelled() => return Err(McpError::internal_error("The tool call was cancelled.", None)),
310        };
311        let CallToolResponse::Complete(result) = response else {
312            return Ok(response);
313        };
314
315        let invalid_params = result.content.first()
316            .and_then(|content| content.as_text())
317            .and_then(|content| content.text.strip_prefix(PARAMS_ERROR_PREFIX))
318            .filter(|_| result.is_error == Some(true));
319
320        Ok(match invalid_params {
321            Some(details) => error_result(&ApiError::InvalidParams(details.to_owned()).into()).into(),
322            None => result.into(),
323        })
324    }
325
326    fn get_info(&self) -> ServerInfo {
327        let capabilities = ServerCapabilities::builder()
328            .enable_tools()
329            .enable_prompts()
330            .enable_resources()
331            .enable_tasks()
332            .build();
333
334        // `ServerInfo` is `#[non_exhaustive]` in rmcp, so it must be built through its constructor instead of a struct literal.
335        let server_info = Implementation::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
336            .with_title("Rusted PackFile Manager")
337            .with_website_url("https://github.com/Frodo45127/rpfm");
338
339        ServerInfo::new(capabilities).with_server_info(server_info).with_instructions("\
340This is the MCP server for RPFM (Rusted PackFile Manager), a tool for modding Total War games by \
341Creative Assembly. It lets you read, edit, create, and manage PackFiles (.pack) — the archive \
342format used by all modern Total War titles.
343
344## Key Concepts
345
346- **PackFile**: An archive containing game data files (DB tables, localisation, textures, models, etc.). \
347  Mods are distributed as PackFiles.
348- **Session**: all MCP clients share one session. It's kept while it has open packs or unfinished jobs; \
349  after 5 minutes without tool calls and with nothing open, it's reset. If a tool fails because the schema \
350  or the dependencies aren't loaded after a pause, call `set_game` again.
351- **pack_key**: When you open one or more PackFiles, each gets a unique key string. Use `session_status` \
352  to discover available keys. Tools working on a pack take its key as `pack`.
353- **Reading data**: `list_files`, `table_info`, `table_rows` and `table_definition` return small, \
354  paginated, structured results. `read_file` reads any other file, as text, decoded JSON or raw bytes.
355- **Editing tables**: `edit_table` inserts, updates and deletes rows by index, with values by column \
356  name. To change a vanilla table, copy it into your pack with `copy_files` first.
357- **Paths**: tools taking plain path strings treat a path as a file if one exists there, or as a \
358  folder otherwise.
359- **Jobs**: slow tools (`set_game`, `generate_dependencies_cache`, `run_diagnostics`, `run_search`, `optimize_pack`...) run as jobs. \
360  They wait up to 45 seconds and return the job's state, with its result if it finished. If it's still \
361  running, call `wait_for_job` with its ID. Clients supporting MCP tasks get a task for them instead. Other tools called meanwhile wait for the job to end, except during \
362  `run_diagnostics`, which stops for them and starts again after them.
363- **Sources**: where a file is — `{\"pack\": <pack key>}` (an open pack), `\"game_files\"` (vanilla game data), \
364  `\"parent_files\"` (packs the open packs depend on), or `\"assembly_kit\"` (Assembly Kit tables). A file is \
365  referenced as `{\"source\": <source>, \"path\": <path>}`.
366
367## Required Initialization Sequence
368
3691. **Set the game** — Call `set_game` with the game key (e.g. `\"warhammer_3\"`). This loads the \
370   schema and the vanilla data.
3712. **Open a pack** — Call `open_pack` with filesystem path(s), or `new_pack`. Note the returned pack key.
3723. **Verify schema** — Call `session_status`; if `schema_loaded` is false, call `update_schemas` first.
3734. **Verify dependencies** — If `dependencies.vanilla_loaded` is false, `dependencies.error` says why. \
374   Call `generate_dependencies_cache` if the cache is missing or outdated.
375
376## Supported Games
377
378Valid game keys: `shogun_2_complete_edition`, `pharaoh_dynasties`, `pharaoh`, `warhammer_3`, `troy`, `three_kingdoms`, \
379`warhammer_2`, `warhammer`, `thrones_of_britannia`, `attila`, `rome_2`, `shogun_2`, `napoleon`, \
380`empire`, `arena`.
381
382## Common File Path Conventions
383
384- DB tables: `db/<table_name>/<file_name>` (e.g. `db/land_units_tables/my_mod`)
385- Localisation: `text/db/<file_name>.loc`
386- Scripts: `script/<path>.lua`
387- Images: `ui/<path>.png`
388
389## Pack File Types (PFHFileType)
390
391`\"Boot\"`, `\"Release\"`, `\"Patch\"`, `\"Mod\"` (default for mods), `\"Movie\"`.
392
393## Compression Formats
394
395`\"None\"` (default), `\"Lzma1\"` (legacy), `\"Lz4\"` (WH3 6.2+), `\"Zstd\"` (WH3 6.2+), `\"Lz4Block\"` (Shogun 2 Complete Edition PFH3 packs only).
396
397## Resources
398
399Use `resources/list` and `resources/read` to browse reference data: valid enum values, game lists, \
400and example JSON payloads without needing tool calls.
401
402## Responses
403
404Tools return structured results. On failure, they return a tool error with a `code`, a `message` and, \
405in `data`, the `kind` of error (like `pack_not_found` or `schema_not_loaded`).
406")
407    }
408
409    //-----------------------------------------------------------------------//
410    // Tasks
411    //-----------------------------------------------------------------------//
412
413    async fn get_task(&self, request: GetTaskParams, _context: RequestContext<RoleServer>) -> Result<GetTaskResult, McpError> {
414        let (session, job) = self.task_job(&request.task_id)?;
415        let record = session.jobs().record(job).ok_or_else(|| unknown_task(&request.task_id))?;
416        Ok(GetTaskResult::new(job_task(session.id(), &record)))
417    }
418
419    /// Cancels the job of a task if it hasn't started. Running jobs finish anyway, which tasks allow.
420    async fn cancel_task(&self, request: CancelTaskParams, _context: RequestContext<RoleServer>) -> Result<(), McpError> {
421        let (session, job) = self.task_job(&request.task_id)?;
422        match session.jobs().cancel(job) {
423            Err(ApiError::JobNotFound(_)) => Err(unknown_task(&request.task_id)),
424            _ => Ok(()),
425        }
426    }
427
428    //-----------------------------------------------------------------------//
429    // Prompts
430    //-----------------------------------------------------------------------//
431
432    async fn get_prompt(&self, request: GetPromptRequestParams, context: RequestContext<RoleServer>) -> Result<GetPromptResponse, McpError> {
433        self.prompt_router.get_prompt(PromptContext::new(self, request.name, request.arguments, context)).await
434    }
435
436    async fn list_prompts(&self, _request: Option<PaginatedRequestParams>, _context: RequestContext<RoleServer>) -> Result<ListPromptsResult, McpError> {
437        Ok(ListPromptsResult::with_all_items(self.prompt_router.list_all())
438            .with_ttl_ms(MCP_CACHE_TTL_MS)
439            .with_cache_scope(CacheScope::Public))
440    }
441
442    //-----------------------------------------------------------------------//
443    // Resources
444    //-----------------------------------------------------------------------//
445
446    async fn list_resources(
447        &self,
448        _request: Option<PaginatedRequestParams>,
449        _context: RequestContext<RoleServer>,
450    ) -> Result<ListResourcesResult, McpError> {
451        let resources = vec![
452            resource("rpfm://games", "games", "List of all supported Total War game keys.", "application/json"),
453            resource("rpfm://enums/PFHFileType", "PFHFileType", "Valid PackFile type values (Boot, Release, Patch, Mod, Movie).", "application/json"),
454            resource("rpfm://enums/CompressionFormat", "CompressionFormat", "Valid compression format values (None, Lzma1, Lz4, Zstd, Lz4Block).", "application/json"),
455            resource("rpfm://enums/SupportedFormats", "SupportedFormats", "Valid video format values (CaVp8, Ivf).", "application/json"),
456            resource("rpfm://reference/initialization", "Initialization guide", "Step-by-step guide for initializing the RPFM MCP server session.", "text/plain"),
457            resource("rpfm://reference/path_conventions", "Path conventions", "Common file path conventions inside Total War PackFiles.", "text/plain"),
458        ];
459        Ok(ListResourcesResult::with_all_items(resources)
460            .with_ttl_ms(MCP_CACHE_TTL_MS)
461            .with_cache_scope(CacheScope::Public))
462    }
463
464    async fn list_resource_templates(
465        &self,
466        _request: Option<PaginatedRequestParams>,
467        _context: RequestContext<RoleServer>,
468    ) -> Result<ListResourceTemplatesResult, McpError> {
469        Ok(ListResourceTemplatesResult::with_all_items(vec![])
470            .with_ttl_ms(MCP_CACHE_TTL_MS)
471            .with_cache_scope(CacheScope::Public))
472    }
473
474    async fn read_resource(
475        &self,
476        request: ReadResourceRequestParams,
477        _context: RequestContext<RoleServer>,
478    ) -> Result<ReadResourceResponse, McpError> {
479        let uri = &request.uri;
480        let content = match uri.as_str() {
481            "rpfm://games" => serde_json::json!({
482                "supported_games": [
483                    {"key": "shogun_2_complete_edition", "display_name": "Total War: Shogun 2 Complete Edition"},
484                    {"key": "pharaoh_dynasties", "display_name": "Total War: Pharaoh Dynasties"},
485                    {"key": "pharaoh", "display_name": "Total War: Pharaoh"},
486                    {"key": "warhammer_3", "display_name": "Total War: Warhammer III"},
487                    {"key": "troy", "display_name": "A Total War Saga: Troy"},
488                    {"key": "three_kingdoms", "display_name": "Total War: Three Kingdoms"},
489                    {"key": "warhammer_2", "display_name": "Total War: Warhammer II"},
490                    {"key": "warhammer", "display_name": "Total War: Warhammer"},
491                    {"key": "thrones_of_britannia", "display_name": "A Total War Saga: Thrones of Britannia"},
492                    {"key": "attila", "display_name": "Total War: Attila"},
493                    {"key": "rome_2", "display_name": "Total War: Rome II"},
494                    {"key": "shogun_2", "display_name": "Total War: Shogun 2"},
495                    {"key": "napoleon", "display_name": "Total War: Napoleon"},
496                    {"key": "empire", "display_name": "Total War: Empire"},
497                    {"key": "arena", "display_name": "Total War: Arena"}
498                ]
499            }).to_string(),
500
501            "rpfm://enums/PFHFileType" => serde_json::json!({
502                "enum": "PFHFileType",
503                "description": "The type/priority of a PackFile. Games load packs in type order (Boot first, Movie last).",
504                "variants": [
505                    {"name": "Boot", "value": 0, "description": "Core game boot files, loaded first."},
506                    {"name": "Release", "value": 1, "description": "Main game data files."},
507                    {"name": "Patch", "value": 2, "description": "Official patch and update files."},
508                    {"name": "Mod", "value": 3, "description": "User mod files. This is the default for mods."},
509                    {"name": "Movie", "value": 4, "description": "Cinematic and always-loaded files, loaded last."}
510                ],
511                "json_example": "\"Mod\""
512            }).to_string(),
513
514            "rpfm://enums/CompressionFormat" => serde_json::json!({
515                "enum": "CompressionFormat",
516                "description": "Compression algorithm for pack file data.",
517                "variants": [
518                    {"name": "None", "description": "No compression (default)."},
519                    {"name": "Lzma1", "description": "Legacy LZMA compression (all PFH5 games)."},
520                    {"name": "Lz4", "description": "LZ4 compression (Warhammer 3 v6.2+)."},
521                    {"name": "Zstd", "description": "Zstandard compression (Warhammer 3 v6.2+)."},
522                    {"name": "Lz4Block", "description": "Raw LZ4 block compression (Shogun 2 Complete Edition PFH3 packs only)."}
523                ],
524                "json_example": "\"None\""
525            }).to_string(),
526
527            "rpfm://enums/SupportedFormats" => serde_json::json!({
528                "enum": "SupportedFormats",
529                "description": "Video format options for CA VP8 video files.",
530                "variants": [
531                    {"name": "CaVp8", "description": "CA's custom VP8 format (default)."},
532                    {"name": "Ivf", "description": "Standard VP8 IVF format."}
533                ],
534                "json_example": "\"CaVp8\""
535            }).to_string(),
536
537            "rpfm://reference/initialization" => "\
538RPFM MCP Server Initialization Guide
539=====================================
540
541Before you can work with PackFiles, you must initialize the server session:
542
543Step 1: Set the game
544    Call: set_game(game: \"warhammer_3\")
545    This loads the correct schemas and vanilla game data for the selected title.
546    Valid game keys: shogun_2_complete_edition, pharaoh_dynasties, pharaoh, warhammer_3, troy, three_kingdoms,
547    warhammer_2, warhammer, thrones_of_britannia, attila, rome_2, shogun_2,
548    napoleon, empire, arena.
549
550Step 2: Verify schema is loaded
551    Call: session_status()
552    If schema_loaded is false, call update_schemas() to download the latest schemas.
553
554Step 3: Open a PackFile
555    Call: open_pack(paths: [\"/path/to/my_mod.pack\"])
556    The response includes the pack key you'll use for all subsequent operations.
557
558Step 4: Verify dependencies (optional but recommended)
559    Call: session_status()
560    If dependencies.vanilla_loaded is false, dependencies.error says why. If the cache is missing or outdated,
561    call generate_dependencies_cache() to build the dependency database.
562
563After initialization, use session_status() to see all open pack keys at any time.
564".to_string(),
565
566            "rpfm://reference/path_conventions" => "\
567Total War PackFile Path Conventions
568====================================
569
570Files inside PackFiles follow specific path conventions:
571
572DB Tables:
573    db/<table_name>/<file_name>
574    Example: db/land_units_tables/my_mod
575    Example: db/unit_stats_land_tables/custom_units
576
577Localisation (Loc) files:
578    text/db/<file_name>.loc
579    text/<file_name>.loc
580    Example: text/db/my_mod.loc
581
582Scripts:
583    script/<path>.lua
584    script/campaign/mod/<script_name>.lua
585    Example: script/campaign/mod/my_mod_script.lua
586    After editing a script, run `run_diagnostics`: it reports Lua syntax errors, invalid DB keys, unknown methods,
587    wrong argument counts and unknown events (all but syntax errors need the game's Assembly Kit installed).
588    To check what a script does, write tests for it and run them with `run_lua_tests`.
589
590UI Images:
591    ui/<path>.png
592    Path may vary depending on the purpose of the image.
593
594Models and Animations:
595    variantmeshes/<path>
596    animations/<path>
597    Example: variantmeshes/wh_variantmodels/hu1/my_unit/my_unit.wsmodel
598
599Audio:
600    audio/<path>.bnk
601
602Maps:
603    terrain/tiles/battle/<map_name>/
604".to_string(),
605
606            _ => {
607                return Err(McpError {
608                    code: ErrorCode::INVALID_PARAMS,
609                    message: format!("Unknown resource URI: {uri}").into(),
610                    data: None,
611                });
612            }
613        };
614
615        Ok(ReadResourceResult::new(vec![ResourceContents::text(content, uri.clone())])
616            .with_ttl_ms(MCP_CACHE_TTL_MS)
617            .with_cache_scope(CacheScope::Public)
618            .into())
619    }
620
621}
622
623impl McpServer {
624
625    /// Runs a request of the API on the session. Requests running as jobs go through [`McpServer::call_job`] instead.
626    ///
627    /// # Returns
628    ///
629    /// The response as structured content, or the error as a tool error, so a failed request doesn't end the MCP session.
630    async fn call_api<R: Request>(&self, tool_name: &str, request: R) -> Result<CallToolResult, McpError> {
631        let tx = start_tool_transaction(tool_name);
632        let session = self.sessions.mcp_session();
633        let result = match send_request(&session, &request).await? {
634
635            // Waiting for a job ends it like the tool that started it, so a failed job is a tool error too.
636            RpcOutcome::Result(value) if R::METHOD == WaitForJob::METHOD => match serde_json::from_value::<JobStatus>(value) {
637                Ok(status) => job_status_result(&status),
638                Err(error) => error_result(&ApiError::Internal(error.to_string()).into()),
639            },
640            RpcOutcome::Result(value) => CallToolResult::structured(value),
641            RpcOutcome::Error(error) => error_result(&error),
642        };
643
644        tx.finish();
645        Ok(result)
646    }
647
648    /// Runs a request of the API that runs as a job on the session.
649    ///
650    /// # Returns
651    ///
652    /// A task for the job if the client supports tasks. Otherwise, the state of the job after waiting for it a while,
653    /// or its error as a tool error if it failed.
654    async fn call_job<R: Request>(&self, tool_name: &str, request: R, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
655        let tx = start_tool_transaction(tool_name);
656        let session = self.sessions.mcp_session();
657        let supports_tasks = context.client_capabilities().is_some_and(|capabilities| capabilities.supports_tasks());
658        let response = match send_request(&session, &request).await? {
659            RpcOutcome::Result(value) => match serde_json::from_value::<JobStarted>(value) {
660                Ok(started) if supports_tasks => match session.jobs().record(started.job) {
661                    Some(record) => CallToolResponse::Task(CreateTaskResult::new(job_task(session.id(), &record).task)),
662                    None => error_result(&ApiError::JobNotFound(started.job).into()).into(),
663                },
664                Ok(started) => match wait_reporting_progress(session.jobs(), started.job, &context).await {
665                    Some(status) => job_status_result(&status).into(),
666                    None => error_result(&ApiError::JobNotFound(started.job).into()).into(),
667                },
668                Err(error) => error_result(&ApiError::Internal(error.to_string()).into()).into(),
669            },
670            RpcOutcome::Error(error) => error_result(&error).into(),
671        };
672
673        tx.finish();
674        Ok(JobToolResponse(response))
675    }
676
677    /// Returns the MCP session and the job of a task.
678    ///
679    /// # Errors
680    ///
681    /// Fails if the task ID isn't of a job of the current MCP session.
682    fn task_job(&self, task_id: &str) -> Result<(Arc<Session>, u64), McpError> {
683        let session = self.sessions.mcp_session();
684        match parse_task_id(task_id) {
685            Some((session_id, job)) if session_id == session.id() => Ok((session, job)),
686            _ => Err(unknown_task(task_id)),
687        }
688    }
689}
690
691#[tool_router]
692impl McpServer {
693
694    #[tool(
695        name = "session_status",
696        description = "Get the state of the session: the selected game, if its schema and dependencies (vanilla files, Assembly Kit tables, parent packs) are loaded, and the open packs with their keys. Call this first to know what's available.",
697        annotations(read_only_hint = true),
698        output_schema = schema_for_output::<SessionStatus>(),
699    )]
700    pub async fn session_status(&self) -> Result<CallToolResult, McpError> {
701        self.call_api("session_status", GetSessionStatus {}).await
702    }
703
704    #[tool(
705        name = "pack_info",
706        description = "Get the details of an open pack: type, format version, compression, encryption flags, the packs it depends on, and its MyMod mode.",
707        annotations(read_only_hint = true),
708        output_schema = schema_for_output::<PackDetails>(),
709    )]
710    pub async fn pack_info(&self, params: Parameters<GetPackInfo>) -> Result<CallToolResult, McpError> {
711        self.call_api("pack_info", params.0).await
712    }
713
714    #[tool(
715        name = "list_files",
716        description = "List the files of an open pack, the game files, the parent packs, or the Assembly Kit tables, sorted by path. Filter by path prefix and file type, and page through big sources with offset/limit (the total is always returned). With recursive=false, the subfolders of the prefix are listed instead of their files, to browse folder by folder.",
717        annotations(read_only_hint = true),
718        output_schema = schema_for_output::<FileList>(),
719    )]
720    pub async fn list_files(&self, params: Parameters<ListFiles>) -> Result<CallToolResult, McpError> {
721        self.call_api("list_files", params.0).await
722    }
723
724    #[tool(
725        name = "table_info",
726        description = "Get the columns (name, type, key, referenced table without its `_tables` suffix and column, lookups, default value, file paths, description, if unused) and the row count of a DB or Loc table, from any source. Columns are listed in the order row values are returned and written.",
727        annotations(read_only_hint = true),
728        output_schema = schema_for_output::<TableInfo>(),
729    )]
730    pub async fn table_info(&self, params: Parameters<GetTableInfo>) -> Result<CallToolResult, McpError> {
731        self.call_api("table_info", params.0).await
732    }
733
734    #[tool(
735        name = "table_rows",
736        description = "Read rows of a DB or Loc table, from any source, as plain values (booleans, numbers and strings). Pick only the columns you need, filter rows by column values, and page with offset/limit (100 rows by default; the total of matching rows is always returned). Each row includes its index in the table.",
737        annotations(read_only_hint = true),
738        output_schema = schema_for_output::<TableRows>(),
739    )]
740    pub async fn table_rows(&self, params: Parameters<GetTableRows>) -> Result<CallToolResult, McpError> {
741        self.call_api("table_rows", params.0).await
742    }
743
744    #[tool(
745        name = "table_definition",
746        description = "Get the columns of a table as defined in the schema, without needing a file of it. Without a version, the version of the table in the game files is used.",
747        annotations(read_only_hint = true),
748        output_schema = schema_for_output::<TableDefinition>(),
749    )]
750    pub async fn table_definition(&self, params: Parameters<GetTableDefinition>) -> Result<CallToolResult, McpError> {
751        self.call_api("table_definition", params.0).await
752    }
753
754    #[tool(
755        name = "set_game",
756        description = "Select the game to work with, like `warhammer_3`, loading its schema and, by default, its dependencies (vanilla files, Assembly Kit tables, parent packs). Call this before opening packs. Runs as a job: waits up to 45 seconds and returns its state, with the session status as result if it finished; if it's still running, call `wait_for_job`.",
757        annotations(read_only_hint = false, destructive_hint = false),
758        output_schema = schema_for_output::<JobStatus>(),
759    )]
760    pub async fn set_game(&self, params: Parameters<SetGame>, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
761        self.call_job("set_game", params.0, context).await
762    }
763
764    #[tool(
765        name = "generate_dependencies_cache",
766        description = "Generate the dependencies cache of the selected game from its files and Assembly Kit, and load it. Needed once per game, and after game updates. Takes a while. Runs as a job: waits up to 45 seconds and returns its state; if it's still running, call `wait_for_job`.",
767        annotations(read_only_hint = false, destructive_hint = false),
768        output_schema = schema_for_output::<JobStatus>(),
769    )]
770    pub async fn generate_dependencies_cache(&self, params: Parameters<GenerateDependenciesCache>, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
771        self.call_job("generate_dependencies_cache", params.0, context).await
772    }
773
774    #[tool(
775        name = "rebuild_dependencies",
776        description = "Reload the dependencies of the selected game, like after changing the packs the open packs depend on. Runs as a job: waits up to 45 seconds and returns its state; if it's still running, call `wait_for_job`.",
777        annotations(read_only_hint = false, destructive_hint = false),
778        output_schema = schema_for_output::<JobStatus>(),
779    )]
780    pub async fn rebuild_dependencies(&self, params: Parameters<RebuildDependencies>, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
781        self.call_job("rebuild_dependencies", params.0, context).await
782    }
783
784    #[tool(
785        name = "dependency_tables",
786        description = "List the tables of the selected game's files, and the startpos and twad tables of its schema, with the version new tables of each type should use. Empty if the dependencies aren't loaded.",
787        annotations(read_only_hint = true),
788        output_schema = schema_for_output::<DependencyTables>(),
789    )]
790    pub async fn dependency_tables(&self, params: Parameters<ListDependencyTables>) -> Result<CallToolResult, McpError> {
791        self.call_api("dependency_tables", params.0).await
792    }
793
794    #[tool(
795        name = "run_diagnostics",
796        description = "Check the open packs for problems (invalid references, duplicated keys, outdated tables, script errors, etc.), and return a summary by level and type. The results are kept: read them with `list_diagnostics`. After fixing some files, pass their `paths` to check only them again. Runs as a job: waits up to 45 seconds and returns its state; if it's still running, call `wait_for_job`.",
797        annotations(read_only_hint = true),
798        output_schema = schema_for_output::<JobStatus>(),
799    )]
800    pub async fn run_diagnostics_tool(&self, params: Parameters<RunDiagnostics>, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
801        self.call_job("run_diagnostics", params.0, context).await
802    }
803
804    #[tool(
805        name = "list_diagnostics",
806        description = "List results of the last `run_diagnostics`, filtered by level, report type, pack and path prefix, in pages (100 by default; the total of matching results is always returned). Table results include the affected cells as [row, column], to fix them with `edit_table`.",
807        annotations(read_only_hint = true),
808        output_schema = schema_for_output::<DiagnosticList>(),
809    )]
810    pub async fn list_diagnostics(&self, params: Parameters<ListDiagnostics>) -> Result<CallToolResult, McpError> {
811        self.call_api("list_diagnostics", params.0).await
812    }
813
814    #[tool(
815        name = "ignore_diagnostics",
816        description = "Make the next diagnostics checks of a pack skip results of files under a path, optionally only for some columns and report types. Saved in the pack's settings, so it applies after saving the pack.",
817        annotations(read_only_hint = false, destructive_hint = false),
818        output_schema = schema_for_output::<Done>(),
819    )]
820    pub async fn ignore_diagnostics(&self, params: Parameters<IgnoreDiagnostics>) -> Result<CallToolResult, McpError> {
821        self.call_api("ignore_diagnostics", params.0).await
822    }
823
824    #[tool(
825        name = "run_search",
826        description = "Search text (or a regex) in open packs, the game files, the parent packs, the Assembly Kit tables, or the schema's column names, and return a summary of the matches by file type. The matches are kept: read them with `list_search_matches`, and replace them with `replace_search_matches`. Runs as a job: waits up to 45 seconds and returns its state; if it's still running, call `wait_for_job`.",
827        annotations(read_only_hint = true),
828        output_schema = schema_for_output::<JobStatus>(),
829    )]
830    pub async fn run_search(&self, params: Parameters<RunSearch>, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
831        self.call_job("run_search", params.0, context).await
832    }
833
834    #[tool(
835        name = "list_search_matches",
836        description = "List matches of the last `run_search`, filtered by file type and path prefix, in pages (100 by default; the total of matching matches is always returned). Each match has an ID to replace it, and details with where it is in its file (row and column for tables, row for text files).",
837        annotations(read_only_hint = true),
838        output_schema = schema_for_output::<SearchMatchList>(),
839    )]
840    pub async fn list_search_matches(&self, params: Parameters<ListSearchMatches>) -> Result<CallToolResult, McpError> {
841        self.call_api("list_search_matches", params.0).await
842    }
843
844    #[tool(
845        name = "replace_search_matches",
846        description = "Replace matches of the last `run_search` by ID, or all of them, with a text. Only matches in open packs are replaced. The edited files are searched again, so match IDs change afterwards: list them again before replacing more.",
847        annotations(read_only_hint = false, destructive_hint = true),
848        output_schema = schema_for_output::<SearchReplaced>(),
849    )]
850    pub async fn replace_search_matches(&self, params: Parameters<ReplaceSearchMatches>) -> Result<CallToolResult, McpError> {
851        self.call_api("replace_search_matches", params.0).await
852    }
853
854    #[tool(
855        name = "column_values",
856        description = "Get the distinct values of a column of a table, like all the faction keys of `factions_tables`, from the open packs and the dependencies, sorted. Filter by prefix, and page with offset/limit (500 by default; the total is always returned). Useful to find valid values for a referenced column: `table_info` tells which table and column a column references.",
857        annotations(read_only_hint = true),
858        output_schema = schema_for_output::<ColumnValues>(),
859    )]
860    pub async fn column_values(&self, params: Parameters<GetColumnValues>) -> Result<CallToolResult, McpError> {
861        self.call_api("column_values", params.0).await
862    }
863
864    #[tool(
865        name = "unused_numbers",
866        description = "Get numbers no row uses yet in an integer column of a table, like `unique_index` of `main_units_tables`, for new rows that need unique ones. They're above the highest one used in the open packs and the dependencies.",
867        annotations(read_only_hint = true),
868        output_schema = schema_for_output::<UnusedNumbers>(),
869    )]
870    pub async fn unused_numbers(&self, params: Parameters<GetUnusedNumbers>) -> Result<CallToolResult, McpError> {
871        self.call_api("unused_numbers", params.0).await
872    }
873
874    #[tool(
875        name = "find_definition",
876        description = "Find the row where a value of a referenced table is defined, like the row of `factions_tables` with a faction key. Searches the open packs, the parent packs, the game files and the Assembly Kit tables, or only `pack` if set, and returns the first row found, with its values.",
877        annotations(read_only_hint = true),
878        output_schema = schema_for_output::<RowLocation>(),
879    )]
880    pub async fn find_definition(&self, params: Parameters<FindDefinition>) -> Result<CallToolResult, McpError> {
881        self.call_api("find_definition", params.0).await
882    }
883
884    #[tool(
885        name = "find_usages",
886        description = "Find the rows of other tables referencing a value of a table, like everything using a faction key. The referencing columns are taken from the schema. Searches the open packs, the parent packs and the game files, or only `pack` if set, and returns each row with its values, in pages (200 by default; the total is always returned). To copy the rows for a new value, insert copies of them with `edit_table` and `copy_from`.",
887        annotations(read_only_hint = true),
888        output_schema = schema_for_output::<Usages>(),
889    )]
890    pub async fn find_usages(&self, params: Parameters<FindUsages>) -> Result<CallToolResult, McpError> {
891        self.call_api("find_usages", params.0).await
892    }
893
894    #[tool(
895        name = "find_loc",
896        description = "Find the row of a Loc file with a key. Searches the open packs, the parent packs and the game files, or only `pack` if set, and returns the first row found, with its values.",
897        annotations(read_only_hint = true),
898        output_schema = schema_for_output::<RowLocation>(),
899    )]
900    pub async fn find_loc(&self, params: Parameters<FindLoc>) -> Result<CallToolResult, McpError> {
901        self.call_api("find_loc", params.0).await
902    }
903
904    #[tool(
905        name = "loc_source",
906        description = "Get the table, localised column and key values a loc key belongs to, like `factions_tables`, `screen_name` and `[\"wh_main_emp_empire\"]` for `factions_screen_name_wh_main_emp_empire`. `source` is null if it can't be found.",
907        annotations(read_only_hint = true),
908        output_schema = schema_for_output::<LocSourceLookup>(),
909    )]
910    pub async fn loc_source(&self, params: Parameters<GetLocSource>) -> Result<CallToolResult, McpError> {
911        self.call_api("loc_source", params.0).await
912    }
913
914    #[tool(
915        name = "row_locs",
916        description = "Get the loc entries of a row of a DB table, from any source: the loc key of each of its localised columns, like `land_units_onscreen_name_my_unit` for `onscreen_name`, with its text, or null if no Loc file has it. Use it to know which loc keys a new row needs.",
917        annotations(read_only_hint = true),
918        output_schema = schema_for_output::<RowLocs>(),
919    )]
920    pub async fn row_locs(&self, params: Parameters<GetRowLocs>) -> Result<CallToolResult, McpError> {
921        self.call_api("row_locs", params.0).await
922    }
923
924    #[tool(
925        name = "pack_settings",
926        description = "Get the settings of an open pack, like its diagnostics ignore rules (`diagnostics_files_to_ignore`), files to ignore when importing, or if it has autosaves disabled.",
927        annotations(read_only_hint = true),
928        output_schema = schema_for_output::<PackSettingsValues>(),
929    )]
930    pub async fn pack_settings(&self, params: Parameters<GetPackSettings>) -> Result<CallToolResult, McpError> {
931        self.call_api("pack_settings", params.0).await
932    }
933
934    #[tool(
935        name = "update_pack_settings",
936        description = "Change settings of an open pack. Only the keys you set are changed. Returns all the settings afterwards.",
937        annotations(read_only_hint = false, destructive_hint = true),
938        output_schema = schema_for_output::<PackSettingsValues>(),
939    )]
940    pub async fn update_pack_settings(&self, params: Parameters<UpdatePackSettings>) -> Result<CallToolResult, McpError> {
941        self.call_api("update_pack_settings", params.0).await
942    }
943
944    #[tool(
945        name = "list_notes",
946        description = "Get the notes (comments) attached to a file or folder of an open pack, or all of them.",
947        annotations(read_only_hint = true),
948        output_schema = schema_for_output::<NoteList>(),
949    )]
950    pub async fn list_notes(&self, params: Parameters<ListNotes>) -> Result<CallToolResult, McpError> {
951        self.call_api("list_notes", params.0).await
952    }
953
954    #[tool(
955        name = "add_note",
956        description = "Attach a note (comment, with an optional link) to a file or folder of an open pack, or replace one by passing its `id`.",
957        annotations(read_only_hint = false, destructive_hint = false),
958        output_schema = schema_for_output::<NoteEntry>(),
959    )]
960    pub async fn add_note(&self, params: Parameters<AddNote>) -> Result<CallToolResult, McpError> {
961        self.call_api("add_note", params.0).await
962    }
963
964    #[tool(
965        name = "delete_note",
966        description = "Delete a note of an open pack.",
967        annotations(read_only_hint = false, destructive_hint = true),
968        output_schema = schema_for_output::<Done>(),
969    )]
970    pub async fn delete_note(&self, params: Parameters<DeleteNote>) -> Result<CallToolResult, McpError> {
971        self.call_api("delete_note", params.0).await
972    }
973
974    #[tool(
975        name = "schema_tables",
976        description = "List the tables of the selected game's schema whose name starts with a prefix, with the versions it has definitions for, newest first.",
977        annotations(read_only_hint = true),
978        output_schema = schema_for_output::<SchemaTables>(),
979    )]
980    pub async fn schema_tables(&self, params: Parameters<ListSchemaTables>) -> Result<CallToolResult, McpError> {
981        self.call_api("schema_tables", params.0).await
982    }
983
984    #[tool(
985        name = "patch_column",
986        description = "Change how the schema describes a column with a local patch: description, if it's a key, default value, referenced table and column (`is_reference` as `table;column`), if it holds file paths, if it can't be empty, or if it's unused. Local patches survive schema updates, and apply right away.",
987        annotations(read_only_hint = false, destructive_hint = false),
988        output_schema = schema_for_output::<Done>(),
989    )]
990    pub async fn patch_column(&self, params: Parameters<PatchColumn>) -> Result<CallToolResult, McpError> {
991        self.call_api("patch_column", params.0).await
992    }
993
994    #[tool(
995        name = "remove_patches",
996        description = "Remove the local schema patches of a table, or of one of its columns.",
997        annotations(read_only_hint = false, destructive_hint = true),
998        output_schema = schema_for_output::<Done>(),
999    )]
1000    pub async fn remove_patches(&self, params: Parameters<RemovePatches>) -> Result<CallToolResult, McpError> {
1001        self.call_api("remove_patches", params.0).await
1002    }
1003
1004    #[tool(
1005        name = "update_schemas",
1006        description = "Download the latest schemas, reload the selected game's one, and rebuild the dependencies. Runs as a job: waits up to 45 seconds and returns its state; if it's still running, call `wait_for_job`.",
1007        annotations(read_only_hint = false, destructive_hint = false),
1008        output_schema = schema_for_output::<JobStatus>(),
1009    )]
1010    pub async fn update_schemas(&self, params: Parameters<UpdateSchemas>, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
1011        self.call_job("update_schemas", params.0, context).await
1012    }
1013
1014    #[tool(
1015        name = "update_schema_from_assembly_kit",
1016        description = "Update the selected game's schema with the table definitions of its Assembly Kit, and save it. For schema maintainers. Runs as a job: waits up to 45 seconds and returns its state; if it's still running, call `wait_for_job`.",
1017        annotations(read_only_hint = false, destructive_hint = false),
1018        output_schema = schema_for_output::<JobStatus>(),
1019    )]
1020    pub async fn update_schema_from_assembly_kit(&self, params: Parameters<UpdateSchemaFromAssemblyKit>, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
1021        self.call_job("update_schema_from_assembly_kit", params.0, context).await
1022    }
1023
1024    #[tool(
1025        name = "merge_tables",
1026        description = "Merge tables of the same type of an open pack into a new one. With `delta: true`, rows are merged by key against the vanilla data; rows that can't be reconciled are returned as conflicts and nothing is written, so call it again with `resolutions` for them.",
1027        annotations(read_only_hint = false, destructive_hint = true),
1028        output_schema = schema_for_output::<TablesMerged>(),
1029    )]
1030    pub async fn merge_tables(&self, params: Parameters<MergeTables>) -> Result<CallToolResult, McpError> {
1031        self.call_api("merge_tables", params.0).await
1032    }
1033
1034    #[tool(
1035        name = "upgrade_table",
1036        description = "Update a table of an open pack to the version it has in the game files, after a game update. Returns the columns removed and added.",
1037        annotations(read_only_hint = false, destructive_hint = true),
1038        output_schema = schema_for_output::<TableUpgraded>(),
1039    )]
1040    pub async fn upgrade_table(&self, params: Parameters<UpgradeTable>) -> Result<CallToolResult, McpError> {
1041        self.call_api("upgrade_table", params.0).await
1042    }
1043
1044    #[tool(
1045        name = "rename_key",
1046        description = "Change a key value of a table in every table of an open pack: the key itself, the columns referencing it, and the loc keys generated from it. Use it to rename things safely.",
1047        annotations(read_only_hint = false, destructive_hint = true),
1048        output_schema = schema_for_output::<FilesEdited>(),
1049    )]
1050    pub async fn rename_key(&self, params: Parameters<RenameKey>) -> Result<CallToolResult, McpError> {
1051        self.call_api("rename_key", params.0).await
1052    }
1053
1054    #[tool(
1055        name = "add_key_deletes",
1056        description = "Add keys to a key deletes table (`db/twad_key_deletes_tables/<file_name>`) of an open pack, to delete those keys of a table in the game.",
1057        annotations(read_only_hint = false, destructive_hint = false),
1058        output_schema = schema_for_output::<FilesEdited>(),
1059    )]
1060    pub async fn add_key_deletes(&self, params: Parameters<AddKeyDeletes>) -> Result<CallToolResult, McpError> {
1061        self.call_api("add_key_deletes", params.0).await
1062    }
1063
1064    #[tool(
1065        name = "export_tsv",
1066        description = "Write a table of an open pack, the game files or the parent packs to a TSV file, to edit it in a spreadsheet.",
1067        annotations(read_only_hint = false, destructive_hint = true),
1068        output_schema = schema_for_output::<Done>(),
1069    )]
1070    pub async fn export_tsv(&self, params: Parameters<ExportTsv>) -> Result<CallToolResult, McpError> {
1071        self.call_api("export_tsv", params.0).await
1072    }
1073
1074    #[tool(
1075        name = "import_tsv",
1076        description = "Replace a table of an open pack with the contents of a TSV file, keeping its GUID.",
1077        annotations(read_only_hint = false, destructive_hint = true),
1078        output_schema = schema_for_output::<TableEdited>(),
1079    )]
1080    pub async fn import_tsv(&self, params: Parameters<ImportTsv>) -> Result<CallToolResult, McpError> {
1081        self.call_api("import_tsv", params.0).await
1082    }
1083
1084    #[tool(
1085        name = "optimizer_options",
1086        description = "Get the optimizer options as the server's settings have them, by name. Use the names to change them in `optimize_pack`.",
1087        annotations(read_only_hint = true),
1088        output_schema = schema_for_output::<OptimizerOptionValues>(),
1089    )]
1090    pub async fn optimizer_options(&self) -> Result<CallToolResult, McpError> {
1091        self.call_api("optimizer_options", GetOptimizerOptions {}).await
1092    }
1093
1094    #[tool(
1095        name = "patch_siege_ai",
1096        description = "Patch the siege maps of an open pack so the AI can use them. Warhammer games only.",
1097        annotations(read_only_hint = false, destructive_hint = true),
1098        output_schema = schema_for_output::<SiegeAiPatched>(),
1099    )]
1100    pub async fn patch_siege_ai(&self, params: Parameters<PatchSiegeAi>) -> Result<CallToolResult, McpError> {
1101        self.call_api("patch_siege_ai", params.0).await
1102    }
1103
1104    #[tool(
1105        name = "pack_map",
1106        description = "Add the tiles and tile maps of a map exported by Terry to an open pack.",
1107        annotations(read_only_hint = false, destructive_hint = false),
1108        output_schema = schema_for_output::<FilesChanged>(),
1109    )]
1110    pub async fn pack_map(&self, params: Parameters<PackMap>) -> Result<CallToolResult, McpError> {
1111        self.call_api("pack_map", params.0).await
1112    }
1113
1114    #[tool(
1115        name = "generate_missing_locs",
1116        description = "Add empty loc entries for the localised columns of the tables of the open packs that don't have them yet.",
1117        annotations(read_only_hint = false, destructive_hint = false),
1118        output_schema = schema_for_output::<FilesEdited>(),
1119    )]
1120    pub async fn generate_missing_locs(&self, params: Parameters<GenerateMissingLocs>) -> Result<CallToolResult, McpError> {
1121        self.call_api("generate_missing_locs", params.0).await
1122    }
1123
1124    #[tool(
1125        name = "update_anim_ids",
1126        description = "Offset the animation ids of an open pack from a starting id, like after a game update moves them.",
1127        annotations(read_only_hint = false, destructive_hint = true),
1128        output_schema = schema_for_output::<FilesEdited>(),
1129    )]
1130    pub async fn update_anim_ids(&self, params: Parameters<UpdateAnimIds>) -> Result<CallToolResult, McpError> {
1131        self.call_api("update_anim_ids", params.0).await
1132    }
1133
1134    #[tool(
1135        name = "anims_by_skeleton",
1136        description = "Get the paths of the animations using a skeleton, in the open packs and the dependencies.",
1137        annotations(read_only_hint = true),
1138        output_schema = schema_for_output::<FilePaths>(),
1139    )]
1140    pub async fn anims_by_skeleton(&self, params: Parameters<AnimsBySkeleton>) -> Result<CallToolResult, McpError> {
1141        self.call_api("anims_by_skeleton", params.0).await
1142    }
1143
1144    #[tool(
1145        name = "export_gltf",
1146        description = "Export a RigidModel of any source to a glTF file, with its textures.",
1147        annotations(read_only_hint = false, destructive_hint = true),
1148        output_schema = schema_for_output::<Done>(),
1149    )]
1150    pub async fn export_gltf(&self, params: Parameters<ExportGltf>) -> Result<CallToolResult, McpError> {
1151        self.call_api("export_gltf", params.0).await
1152    }
1153
1154    #[tool(
1155        name = "set_video_format",
1156        description = "Change the format of a ca_vp8 video of an open pack: `CaVp8` or `Ivf`.",
1157        annotations(read_only_hint = false, destructive_hint = true),
1158        output_schema = schema_for_output::<Done>(),
1159    )]
1160    pub async fn set_video_format(&self, params: Parameters<SetVideoFormat>) -> Result<CallToolResult, McpError> {
1161        self.call_api("set_video_format", params.0).await
1162    }
1163
1164    #[tool(
1165        name = "live_export",
1166        description = "Export the scripts and UI files of an open pack to the game's data folder, to test them without saving the pack.",
1167        annotations(read_only_hint = false, destructive_hint = true),
1168        output_schema = schema_for_output::<Done>(),
1169    )]
1170    pub async fn live_export(&self, params: Parameters<LiveExport>) -> Result<CallToolResult, McpError> {
1171        self.call_api("live_export", params.0).await
1172    }
1173
1174    #[tool(
1175        name = "init_mymod",
1176        description = "Create the folder of a new MyMod in the MyMods folder of the settings, with optional editor configs for Lua scripting and a git repository.",
1177        annotations(read_only_hint = false, destructive_hint = false),
1178        output_schema = schema_for_output::<MyModCreated>(),
1179    )]
1180    pub async fn init_mymod(&self, params: Parameters<InitMyMod>) -> Result<CallToolResult, McpError> {
1181        self.call_api("init_mymod", params.0).await
1182    }
1183
1184    #[tool(
1185        name = "startpos_campaigns",
1186        description = "Get the campaigns a startpos can be built for, and the one the pack's last startpos was built for.",
1187        annotations(read_only_hint = true),
1188        output_schema = schema_for_output::<StartposCampaigns>(),
1189    )]
1190    pub async fn startpos_campaigns(&self, params: Parameters<GetStartposCampaigns>) -> Result<CallToolResult, McpError> {
1191        self.call_api("startpos_campaigns", params.0).await
1192    }
1193
1194    #[tool(
1195        name = "start_startpos",
1196        description = "Start building a startpos for a campaign with the tables of an open pack: prepares the Assembly Kit and launches the game. Tell the user to close the game once it's done loading, then call `finish_startpos`.",
1197        annotations(read_only_hint = false, destructive_hint = false),
1198        output_schema = schema_for_output::<Done>(),
1199    )]
1200    pub async fn start_startpos(&self, params: Parameters<StartStartpos>) -> Result<CallToolResult, McpError> {
1201        self.call_api("start_startpos", params.0).await
1202    }
1203
1204    #[tool(
1205        name = "finish_startpos",
1206        description = "Finish building a startpos after the game was closed: imports it into the pack, or cancels the build with `cancel: true`. Cleans up the build files either way.",
1207        annotations(read_only_hint = false, destructive_hint = false),
1208        output_schema = schema_for_output::<FilesEdited>(),
1209    )]
1210    pub async fn finish_startpos(&self, params: Parameters<FinishStartpos>) -> Result<CallToolResult, McpError> {
1211        self.call_api("finish_startpos", params.0).await
1212    }
1213
1214    #[tool(
1215        name = "validate_startpos",
1216        description = "Check the tables a campaign's startpos is built from: broken references and data that make the game's build fail, hang or drop rows. Works for both ways of building startposes.",
1217        annotations(read_only_hint = true),
1218        output_schema = schema_for_output::<StartposValidation>(),
1219    )]
1220    pub async fn validate_startpos(&self, params: Parameters<ValidateStartpos>) -> Result<CallToolResult, McpError> {
1221        self.call_api("validate_startpos", params.0).await
1222    }
1223
1224    #[tool(
1225        name = "generate_startpos",
1226        description = "Generate the startpos of a campaign from the tables of an open pack and the game's files, without launching the game, and add it to the pack. Experimental, Attila only.",
1227        annotations(read_only_hint = false, destructive_hint = false),
1228        output_schema = schema_for_output::<FilesEdited>(),
1229    )]
1230    pub async fn generate_startpos(&self, params: Parameters<GenerateStartpos>) -> Result<CallToolResult, McpError> {
1231        self.call_api("generate_startpos", params.0).await
1232    }
1233
1234    #[tool(
1235        name = "extract_startpos_tables",
1236        description = "Extract the start_pos tables and victory objectives of a campaign from its startpos (the pack's, or the game's) and add them to the pack. The inverse of generate_startpos. Experimental, Attila only.",
1237        annotations(read_only_hint = false, destructive_hint = false),
1238        output_schema = schema_for_output::<FilesEdited>(),
1239    )]
1240    pub async fn extract_startpos_tables(&self, params: Parameters<ExtractStartposTables>) -> Result<CallToolResult, McpError> {
1241        self.call_api("extract_startpos_tables", params.0).await
1242    }
1243
1244    #[tool(
1245        name = "keep_startpos_campaign",
1246        description = "Remove the rows of an open pack's start_pos tables that don't belong to a campaign, as the Assembly Kit does before building a startpos, so tables holding several campaigns don't break the build. Tables left without rows are deleted.",
1247        annotations(read_only_hint = false, destructive_hint = true),
1248        output_schema = schema_for_output::<FilesChanged>(),
1249    )]
1250    pub async fn keep_startpos_campaign(&self, params: Parameters<KeepStartposCampaign>) -> Result<CallToolResult, McpError> {
1251        self.call_api("keep_startpos_campaign", params.0).await
1252    }
1253
1254    #[tool(
1255        name = "compare_startpos_tables",
1256        description = "Compare the tables extracted from a campaign's startpos with the tables it's built from, to find what the build loses or changes. Experimental, Attila only.",
1257        annotations(read_only_hint = true),
1258        output_schema = schema_for_output::<StartposComparison>(),
1259    )]
1260    pub async fn compare_startpos_tables(&self, params: Parameters<CompareStartposTables>) -> Result<CallToolResult, McpError> {
1261        self.call_api("compare_startpos_tables", params.0).await
1262    }
1263
1264    #[tool(
1265        name = "optimize_pack",
1266        description = "Remove data of an open pack that's identical to vanilla or unneeded (duplicated rows, unchanged rows and files, empty tables, etc.). Options not set keep the server's settings; see `optimizer_options`. Runs as a job: waits up to 45 seconds and returns its state; if it's still running, call `wait_for_job`.",
1267        annotations(read_only_hint = false, destructive_hint = true),
1268        output_schema = schema_for_output::<JobStatus>(),
1269    )]
1270    pub async fn optimize_pack(&self, params: Parameters<OptimizePack>, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
1271        self.call_job("optimize_pack", params.0, context).await
1272    }
1273
1274    #[tool(
1275        name = "run_lua_tests",
1276        description = "Run Lua tests against the scripts of all open packs, outside of the game. Scripts run in Lua 5.1 with the game's real script libraries; the game's engine is emulated with objects typed after the Assembly Kit's scripting docs, which record every call made on them. Needs the game's Assembly Kit.
1277
1278Test file API (a global `rpfm` table):
1279- Top-level code runs before the game boots, to set up the world: `local kislev = rpfm.faction { key = \"wh3_main_ksl_kislev\", is_human = true }`. Fields other than `key` are the values returned by the methods with the same name; lists like `region_list` can be plain arrays. Also `rpfm.region { key = ... }`, `rpfm.character { faction = kislev, ... }`, and `rpfm.object(\"TYPE_SCRIPT_INTERFACE\", methods)`. Factions and regions from the game's DB exist even if not set up.
1280- `rpfm.test(name, function)` registers a test. Each test runs in a fresh Lua state, after the libraries, the pack's mods (script/campaign/mod/) and the first tick have run.
1281- `rpfm.fire(event, { accessor = value, ... })` triggers an event, like `rpfm.fire(\"FactionTurnStart\", { faction = kislev })`.
1282- `rpfm.advance_time(seconds)` advances game time, triggering due time triggers, like the ones from `cm:callback`. `rpfm.end_turn()` plays a full round: `WorldStartRound`, `FactionRoundStart` for every faction, then for each faction in creation order its `FactionTurnStart`, the turn events of the regions and characters in its `region_list` and `character_list`, `FactionBeginTurnPhaseNormal`, `FactionAboutToEndTurn` and `FactionTurnEnd`.
1283- `rpfm.mock(object, method, value)` changes what a method of an engine object returns after boot, like `rpfm.mock(region, \"owning_faction\", kislev)`; `value` can be a function receiving the call's arguments. Scripts' own globals (like a mod's manager table) can be inspected and changed directly from tests.
1284- `rpfm.assert_called(method, args...)`, `rpfm.assert_not_called(method)`, `rpfm.calls_to(method)` and `rpfm.assert_equal(actual, expected)` check what the scripts did. Calls on `cm` are recorded by their method name, like `treasury_mod`.
1285
1286The report lists each test with its errors (including errors of the pack's scripts, and in listeners), the undocumented methods it called (whose results are placeholders), and the scripts' output.
1287
1288Runs as a job: waits up to 45 seconds and returns its state, with the report as its result; if it's still running, call `wait_for_job`.",
1289        annotations(read_only_hint = true),
1290        output_schema = schema_for_output::<JobStatus>(),
1291    )]
1292    pub async fn run_lua_tests(&self, params: Parameters<RunLuaTests>, context: RequestContext<RoleServer>) -> Result<JobToolResponse, McpError> {
1293        self.call_job("run_lua_tests", params.0, context).await
1294    }
1295
1296    #[tool(
1297        name = "check_update",
1298        description = "Check if there is an update of RPFM (`program`), the `schemas`, the Lua type definitions (`lua_autogen`), the Empire and Napoleon Assembly Kit data (`old_assembly_kit`), or the community `translations`.",
1299        annotations(read_only_hint = true),
1300        output_schema = schema_for_output::<UpdateStatus>(),
1301    )]
1302    pub async fn check_update(&self, params: Parameters<CheckUpdate>) -> Result<CallToolResult, McpError> {
1303        self.call_api("check_update", params.0).await
1304    }
1305
1306    #[tool(
1307        name = "apply_update",
1308        description = "Download the update of the Lua type definitions (`lua_autogen`), the Empire and Napoleon Assembly Kit data (`old_assembly_kit`), the community `translations`, or RPFM itself (`program`, which replaces its files and needs a restart: only do it if the user asks). Schemas are updated with `update_schemas`.",
1309        annotations(read_only_hint = false, destructive_hint = true),
1310        output_schema = schema_for_output::<Done>(),
1311    )]
1312    pub async fn apply_update(&self, params: Parameters<ApplyUpdate>) -> Result<CallToolResult, McpError> {
1313        self.call_api("apply_update", params.0).await
1314    }
1315
1316    #[tool(
1317        name = "read_file",
1318        description = "Read a file of an open pack, the game files or the parent packs: as `text` for scripts, XML, JSON and other text files; as `decoded` JSON for structured formats (portrait settings, unit variants, models, etc.); or as `raw` base64 bytes, best for images and other binary files. For DB and Loc tables, prefer `table_rows`, which returns only the rows and columns you ask for.",
1319        annotations(read_only_hint = true),
1320        output_schema = schema_for_output::<FileContents>(),
1321    )]
1322    pub async fn read_file(&self, params: Parameters<ReadFile>) -> Result<CallToolResult, McpError> {
1323        self.call_api("read_file", params.0).await
1324    }
1325
1326    #[tool(
1327        name = "write_file",
1328        description = "Replace the contents of a file of an open pack: `text` for text files, `decoded` JSON in the format `read_file` returns, or `raw` base64 bytes (which also creates the file if it doesn't exist). For DB and Loc tables, prefer `edit_table`.",
1329        annotations(read_only_hint = false, destructive_hint = true),
1330        output_schema = schema_for_output::<Done>(),
1331    )]
1332    pub async fn write_file(&self, params: Parameters<WriteFile>) -> Result<CallToolResult, McpError> {
1333        self.call_api("write_file", params.0).await
1334    }
1335
1336    #[tool(
1337        name = "list_animpack",
1338        description = "List the files inside an AnimPack of an open pack, the game files or the parent packs.",
1339        annotations(read_only_hint = true),
1340        output_schema = schema_for_output::<FileList>(),
1341    )]
1342    pub async fn list_animpack(&self, params: Parameters<ListAnimPack>) -> Result<CallToolResult, McpError> {
1343        self.call_api("list_animpack", params.0).await
1344    }
1345
1346    #[tool(
1347        name = "add_to_animpack",
1348        description = "Copy files of an open pack into an AnimPack of an open pack.",
1349        annotations(read_only_hint = false, destructive_hint = false),
1350        output_schema = schema_for_output::<FilesAdded>(),
1351    )]
1352    pub async fn add_to_animpack(&self, params: Parameters<AddToAnimPack>) -> Result<CallToolResult, McpError> {
1353        self.call_api("add_to_animpack", params.0).await
1354    }
1355
1356    #[tool(
1357        name = "extract_from_animpack",
1358        description = "Copy files of an AnimPack of any source into an open pack.",
1359        annotations(read_only_hint = false, destructive_hint = false),
1360        output_schema = schema_for_output::<FilesAdded>(),
1361    )]
1362    pub async fn extract_from_animpack(&self, params: Parameters<ExtractFromAnimPack>) -> Result<CallToolResult, McpError> {
1363        self.call_api("extract_from_animpack", params.0).await
1364    }
1365
1366    #[tool(
1367        name = "delete_from_animpack",
1368        description = "Delete files from an AnimPack of an open pack.",
1369        annotations(read_only_hint = false, destructive_hint = true),
1370        output_schema = schema_for_output::<Done>(),
1371    )]
1372    pub async fn delete_from_animpack(&self, params: Parameters<DeleteFromAnimPack>) -> Result<CallToolResult, McpError> {
1373        self.call_api("delete_from_animpack", params.0).await
1374    }
1375
1376    #[tool(
1377        name = "raw_definitions",
1378        description = "Get the definitions of a table as the schema stores them, to edit them with `set_definition`. Their fields are the raw on-disk layout, which can differ from the columns rows have: use `table_definition` for those.",
1379        annotations(read_only_hint = true),
1380        output_schema = schema_for_output::<RawDefinitions>(),
1381    )]
1382    pub async fn raw_definitions(&self, params: Parameters<GetRawDefinitions>) -> Result<CallToolResult, McpError> {
1383        self.call_api("raw_definitions", params.0).await
1384    }
1385
1386    #[tool(
1387        name = "set_definition",
1388        description = "Add a definition to the selected game's schema, or replace the one with its version, then save and reload the schema. For schema maintainers; use `patch_column` to change column metadata locally instead.",
1389        annotations(read_only_hint = false, destructive_hint = true),
1390        output_schema = schema_for_output::<Done>(),
1391    )]
1392    pub async fn set_definition(&self, params: Parameters<SetDefinition>) -> Result<CallToolResult, McpError> {
1393        self.call_api("set_definition", params.0).await
1394    }
1395
1396    #[tool(
1397        name = "delete_definition",
1398        description = "Remove a definition from the selected game's schema, then save and reload the schema. For schema maintainers.",
1399        annotations(read_only_hint = false, destructive_hint = true),
1400        output_schema = schema_for_output::<Done>(),
1401    )]
1402    pub async fn delete_definition(&self, params: Parameters<DeleteDefinition>) -> Result<CallToolResult, McpError> {
1403        self.call_api("delete_definition", params.0).await
1404    }
1405
1406    #[tool(
1407        name = "referencing_columns",
1408        description = "Get the columns of other tables referencing each column of a table, according to the schema.",
1409        annotations(read_only_hint = true),
1410        output_schema = schema_for_output::<ReferencingColumns>(),
1411    )]
1412    pub async fn referencing_columns(&self, params: Parameters<GetReferencingColumns>) -> Result<CallToolResult, McpError> {
1413        self.call_api("referencing_columns", params.0).await
1414    }
1415
1416    #[tool(
1417        name = "table_patches",
1418        description = "Get the patches applied to the columns of a table definition: the local ones made with `patch_column`, and the ones included in the schema.",
1419        annotations(read_only_hint = true),
1420        output_schema = schema_for_output::<TablePatches>(),
1421    )]
1422    pub async fn table_patches(&self, params: Parameters<GetTablePatches>) -> Result<CallToolResult, McpError> {
1423        self.call_api("table_patches", params.0).await
1424    }
1425
1426    #[tool(
1427        name = "missing_definitions",
1428        description = "List the tables of an open pack with rows the schema can't decode, which need a new definition. Slow on big packs.",
1429        annotations(read_only_hint = true),
1430        output_schema = schema_for_output::<MissingDefinitions>(),
1431    )]
1432    pub async fn missing_definitions(&self, params: Parameters<GetMissingDefinitions>) -> Result<CallToolResult, McpError> {
1433        self.call_api("missing_definitions", params.0).await
1434    }
1435
1436    #[tool(
1437        name = "import_patches",
1438        description = "Add patches to the selected game's schema itself, and save it. Unlike `patch_column`, they're part of the schema, so a schema update replaces them. For schema maintainers.",
1439        annotations(read_only_hint = false, destructive_hint = true),
1440        output_schema = schema_for_output::<Done>(),
1441    )]
1442    pub async fn import_patches(&self, params: Parameters<ImportPatches>) -> Result<CallToolResult, McpError> {
1443        self.call_api("import_patches", params.0).await
1444    }
1445
1446    #[tool(
1447        name = "reference_values",
1448        description = "Get the values a reference column of a table can have, with their display text (like the name of each referenced row), from the open packs and the dependencies. Filter by prefix and page with offset/limit (500 by default).",
1449        annotations(read_only_hint = true),
1450        output_schema = schema_for_output::<ReferenceValues>(),
1451    )]
1452    pub async fn reference_values(&self, params: Parameters<GetReferenceValues>) -> Result<CallToolResult, McpError> {
1453        self.call_api("reference_values", params.0).await
1454    }
1455
1456    #[tool(
1457        name = "list_translations",
1458        description = "Get the translation of the texts of an open pack to a language, reusing vanilla and previous translations. Filter to the `untranslated` entries or the ones that `needs_review`, and page with offset/limit (200 by default).",
1459        annotations(read_only_hint = true),
1460        output_schema = schema_for_output::<Translations>(),
1461    )]
1462    pub async fn list_translations(&self, params: Parameters<ListTranslations>) -> Result<CallToolResult, McpError> {
1463        self.call_api("list_translations", params.0).await
1464    }
1465
1466    #[tool(
1467        name = "generate_vanilla_texts",
1468        description = "Generate the vanilla texts of a language from the game's locale packs, so translations to and from it can reuse them.",
1469        annotations(read_only_hint = false, destructive_hint = false),
1470        output_schema = schema_for_output::<VanillaTextsAvailable>(),
1471    )]
1472    pub async fn generate_vanilla_texts(&self, params: Parameters<GenerateVanillaTexts>) -> Result<CallToolResult, McpError> {
1473        self.call_api("generate_vanilla_texts", params.0).await
1474    }
1475
1476    #[tool(
1477        name = "job_status",
1478        description = "Get the state of a job: queued, running (with its current step), finished (with its result), failed (with its error) or cancelled.",
1479        annotations(read_only_hint = true),
1480        output_schema = schema_for_output::<JobStatus>(),
1481    )]
1482    pub async fn job_status(&self, params: Parameters<GetJobStatus>) -> Result<CallToolResult, McpError> {
1483        self.call_api("job_status", params.0).await
1484    }
1485
1486    #[tool(
1487        name = "wait_for_job",
1488        description = "Wait for a job to end, up to `timeout_secs` (60 by default), and return its state, or its error if it failed.",
1489        annotations(read_only_hint = true),
1490        output_schema = schema_for_output::<JobStatus>(),
1491    )]
1492    pub async fn wait_for_job(&self, params: Parameters<WaitForJob>) -> Result<CallToolResult, McpError> {
1493        self.call_api("wait_for_job", params.0).await
1494    }
1495
1496    #[tool(
1497        name = "cancel_job",
1498        description = "Cancel a job that hasn't started yet. Running jobs can't be cancelled.",
1499        annotations(read_only_hint = false, destructive_hint = true),
1500        output_schema = schema_for_output::<Done>(),
1501    )]
1502    pub async fn cancel_job(&self, params: Parameters<CancelJob>) -> Result<CallToolResult, McpError> {
1503        self.call_api("cancel_job", params.0).await
1504    }
1505
1506    #[tool(
1507        name = "new_pack",
1508        description = "Create a new empty pack, optionally with a file name like `my_mod.pack`. It has no path on disk until you save it with `save_pack` and a `path`.",
1509        annotations(read_only_hint = false, destructive_hint = false),
1510        output_schema = schema_for_output::<PackSummary>(),
1511    )]
1512    pub async fn new_pack(&self, params: Parameters<NewPack>) -> Result<CallToolResult, McpError> {
1513        self.call_api("new_pack", params.0).await
1514    }
1515
1516    #[tool(
1517        name = "open_pack",
1518        description = "Open one or more packs from disk, merged into a single one. Returns its key, used by every tool that works on it.",
1519        annotations(read_only_hint = false, destructive_hint = false),
1520        output_schema = schema_for_output::<PackSummary>(),
1521    )]
1522    pub async fn open_pack(&self, params: Parameters<OpenPack>) -> Result<CallToolResult, McpError> {
1523        self.call_api("open_pack", params.0).await
1524    }
1525
1526    #[tool(
1527        name = "open_vanilla_packs",
1528        description = "Open all the vanilla packs of the selected game, merged into a single one, to browse them like any open pack.",
1529        annotations(read_only_hint = false, destructive_hint = false),
1530        output_schema = schema_for_output::<PackSummary>(),
1531    )]
1532    pub async fn open_vanilla_packs(&self) -> Result<CallToolResult, McpError> {
1533        self.call_api("open_vanilla_packs", OpenVanillaPacks {}).await
1534    }
1535
1536    #[tool(
1537        name = "close_pack",
1538        description = "Close an open pack. Unsaved changes are lost.",
1539        annotations(read_only_hint = false, destructive_hint = true),
1540        output_schema = schema_for_output::<Done>(),
1541    )]
1542    pub async fn close_pack(&self, params: Parameters<ClosePack>) -> Result<CallToolResult, McpError> {
1543        self.call_api("close_pack", params.0).await
1544    }
1545
1546    #[tool(
1547        name = "close_all_packs",
1548        description = "Close all open packs. Unsaved changes are lost.",
1549        annotations(read_only_hint = false, destructive_hint = true),
1550        output_schema = schema_for_output::<Done>(),
1551    )]
1552    pub async fn close_all_packs(&self) -> Result<CallToolResult, McpError> {
1553        self.call_api("close_all_packs", CloseAllPacks {}).await
1554    }
1555
1556    #[tool(
1557        name = "save_pack",
1558        description = "Save an open pack to disk, to its current path or to a new `path`. New packs need a path. Set `clean` to drop files that failed to decode if saving normally fails.",
1559        annotations(read_only_hint = false, destructive_hint = true),
1560        output_schema = schema_for_output::<PackSummary>(),
1561    )]
1562    pub async fn save_pack(&self, params: Parameters<SavePack>) -> Result<CallToolResult, McpError> {
1563        self.call_api("save_pack", params.0).await
1564    }
1565
1566    #[tool(
1567        name = "update_pack",
1568        description = "Change properties of an open pack: type, compression, encryption and timestamp flags, the packs it depends on, and its MyMod mode. Only the fields you set are changed.",
1569        annotations(read_only_hint = false, destructive_hint = true),
1570        output_schema = schema_for_output::<PackDetails>(),
1571    )]
1572    pub async fn update_pack(&self, params: Parameters<UpdatePack>) -> Result<CallToolResult, McpError> {
1573        self.call_api("update_pack", params.0).await
1574    }
1575
1576    #[tool(
1577        name = "create_file",
1578        description = "Create a new empty file in an open pack: a DB table (with the version of the game files by default), a Loc table, a text file or an AnimPack. Fill tables afterwards with `edit_table`.",
1579        annotations(read_only_hint = false, destructive_hint = false),
1580        output_schema = schema_for_output::<FileEntry>(),
1581    )]
1582    pub async fn create_file(&self, params: Parameters<CreateFile>) -> Result<CallToolResult, McpError> {
1583        self.call_api("create_file", params.0).await
1584    }
1585
1586    #[tool(
1587        name = "add_files_from_disk",
1588        description = "Add files and folders from disk to an open pack, under a folder of the pack.",
1589        annotations(read_only_hint = false, destructive_hint = false),
1590        output_schema = schema_for_output::<FilesAdded>(),
1591    )]
1592    pub async fn add_files_from_disk(&self, params: Parameters<AddFilesFromDisk>) -> Result<CallToolResult, McpError> {
1593        self.call_api("add_files_from_disk", params.0).await
1594    }
1595
1596    #[tool(
1597        name = "copy_files",
1598        description = "Copy files and folders from an open pack, the game files, the parent packs or the Assembly Kit tables into an open pack, keeping their paths. Use it to start editing a vanilla table in your mod. Paths that match nothing are returned in `not_added`.",
1599        annotations(read_only_hint = false, destructive_hint = false),
1600        output_schema = schema_for_output::<FilesAdded>(),
1601    )]
1602    pub async fn copy_files(&self, params: Parameters<CopyFiles>) -> Result<CallToolResult, McpError> {
1603        self.call_api("copy_files", params.0).await
1604    }
1605
1606    #[tool(
1607        name = "delete_files",
1608        description = "Delete files and folders from an open pack.",
1609        annotations(read_only_hint = false, destructive_hint = true),
1610        output_schema = schema_for_output::<FilesDeleted>(),
1611    )]
1612    pub async fn delete_files(&self, params: Parameters<DeleteFiles>) -> Result<CallToolResult, McpError> {
1613        self.call_api("delete_files", params.0).await
1614    }
1615
1616    #[tool(
1617        name = "rename_files",
1618        description = "Rename or move files and folders of an open pack.",
1619        annotations(read_only_hint = false, destructive_hint = true),
1620        output_schema = schema_for_output::<FilesRenamed>(),
1621    )]
1622    pub async fn rename_files(&self, params: Parameters<RenameFiles>) -> Result<CallToolResult, McpError> {
1623        self.call_api("rename_files", params.0).await
1624    }
1625
1626    #[tool(
1627        name = "duplicate_files",
1628        description = "Copy files of an open pack in the same pack, adding a number to their names.",
1629        annotations(read_only_hint = false, destructive_hint = false),
1630        output_schema = schema_for_output::<FilesAdded>(),
1631    )]
1632    pub async fn duplicate_files(&self, params: Parameters<DuplicateFiles>) -> Result<CallToolResult, McpError> {
1633        self.call_api("duplicate_files", params.0).await
1634    }
1635
1636    #[tool(
1637        name = "extract_files",
1638        description = "Extract files and folders of an open pack, the game files or the parent packs to a folder on disk, optionally with tables as TSV. Existing files on disk are overwritten.",
1639        annotations(read_only_hint = false, destructive_hint = true),
1640        output_schema = schema_for_output::<FilesExtracted>(),
1641    )]
1642    pub async fn extract_files(&self, params: Parameters<ExtractFiles>) -> Result<CallToolResult, McpError> {
1643        self.call_api("extract_files", params.0).await
1644    }
1645
1646    #[tool(
1647        name = "edit_table",
1648        description = "Edit rows of a DB or Loc table in an open pack: insert, update and delete rows by index, with values given by column name (see `table_info` for the columns). Inserts can copy a row of any table with `copy_from`, like a vanilla one, changing only the values they set. Edits apply in order, each on the result of the previous ones; if any fails, none is applied. To edit a vanilla table, copy it into your pack first with `copy_files`.",
1649        annotations(read_only_hint = false, destructive_hint = true),
1650        output_schema = schema_for_output::<TableEdited>(),
1651    )]
1652    pub async fn edit_table(&self, params: Parameters<EditTable>) -> Result<CallToolResult, McpError> {
1653        self.call_api("edit_table", params.0).await
1654    }
1655
1656    pub fn new(sessions: Arc<SessionManager>) -> Self {
1657        Self {
1658            sessions,
1659            tool_router: McpServer::tool_router(),
1660            prompt_router: McpServer::prompt_router(),
1661        }
1662    }
1663
1664}
1665
1666//-------------------------------------------------------------------------------//
1667//                              MCP Prompts
1668//-------------------------------------------------------------------------------//
1669
1670#[prompt_router]
1671impl McpServer {
1672
1673    #[prompt(name = "open_and_inspect_pack", description = "Walk through opening a PackFile and inspecting its contents.")]
1674    pub async fn open_and_inspect_pack(&self) -> Vec<PromptMessage> {
1675        vec![PromptMessage::new_text(
1676            Role::User,
1677            "\
1678You are an assistant helping the user inspect a Total War PackFile using the RPFM MCP server.
1679
1680Follow these steps in order:
1681
16821. **Select the game** – Call `set_game` with the correct game key (e.g. `\"warhammer_3\"`), so the
1683   schema and the vanilla data are loaded.
1684
16852. **Open the pack** – Call `open_pack` with the filesystem path(s) the user provides.
1686   Remember the returned `key`; the other tools take it as the pack key.
1687
16883. **List pack contents** – Call `list_files` with `source: {\"pack\": <pack key>}`. For big packs,
1689   browse folder by folder with `recursive: false` and a `path_prefix`, or filter by `file_types`.
1690   Present the files to the user in a readable format.
1691
16924. **Read tables** – For DB and Loc tables, call `table_info` to see their columns and row count,
1693   and `table_rows` to read rows, picking only the columns and rows you need with `columns` and
1694   `filters`. For other files, call `read_file`: as `text` for scripts and other text files,
1695   or as `decoded` JSON for the rest.
1696
16975. **Inspect metadata** – Use `pack_info` and `pack_settings` to answer questions about the pack itself.
1698
1699Important notes:
1700- Always call `session_status` if you are unsure which pack key to use.
1701- If a table fails to decode, check `schema_loaded` in `session_status`; if false, call `update_schemas` first.
1702- When done, optionally call `close_pack` to free resources.
1703",
1704        )]
1705    }
1706
1707    #[prompt(name = "edit_db_table", description = "Guide for reading, modifying, and saving a DB table inside a pack.")]
1708    pub async fn edit_db_table(&self) -> Vec<PromptMessage> {
1709        vec![PromptMessage::new_text(
1710            Role::User,
1711            "\
1712You are an assistant helping the user edit a DB table inside a Total War PackFile.
1713
1714Workflow:
1715
17161. **Set the game** – `set_game` with the game key.
17172. **Open the pack** – `open_pack` → note the pack `key`.
17183. **Find the table** – `list_files` with `source: {\"pack\": <pack key>}`, `path_prefix: \"db/\"` and
1719   `file_types: [\"DB\"]`. To edit a vanilla table, copy it into the pack first:
1720   `copy_files` with `source: \"game_files\"`, its path, and the pack key as `pack`.
17214. **Inspect it** – `table_info` with `file: {\"source\": {\"pack\": <pack key>}, \"path\": <DB path>}`
1722   returns its columns (name, type, key, referenced table) and row count.
17235. **Read the rows you need** – `table_rows` with `columns` and `filters`, to find the index of each
1724   row to change.
17256. **Edit** – `edit_table` with the pack key, the path of the table, and a list of edits:
1726   - `{\"op\": \"update\", \"index\": 3, \"values\": {\"column\": value}}`
1727   - `{\"op\": \"insert\", \"values\": {\"key\": \"my_key\", ...}}` (missing columns get their default)
1728   - `{\"op\": \"insert\", \"copy_from\": {\"file\": {\"source\": \"game_files\", \"path\": <DB path>}, \"index\": 12}, \"values\": {\"key\": \"my_key\"}}`
1729     (a copy of another row, with the values it sets changed)
1730   - `{\"op\": \"delete\", \"indexes\": [5, 7]}`
1731   Edits apply in order, each on the result of the previous ones, and if any fails, none is applied.
17327. **Save the pack** – `save_pack` (with a `path` to save it somewhere else).
1733
1734Tips:
1735- Use `reference_values` to discover valid values for referenced columns, with their names.
1736- To make a new entry based on a vanilla one, like a unit, copy its row with `copy_from`, and copy the rows
1737  referencing it too: `find_usages` lists them, so each can be copied with the new key as its only change.
1738- Mods usually only keep the rows they change: `optimize_pack` removes rows identical to vanilla.
1739- After saving, you can run `run_diagnostics` to validate the pack.
1740",
1741        )]
1742    }
1743
1744    #[prompt(name = "create_new_mod", description = "Step-by-step guide for creating a new mod PackFile from scratch.")]
1745    pub async fn create_new_mod(&self) -> Vec<PromptMessage> {
1746        vec![PromptMessage::new_text(
1747            Role::User,
1748            "\
1749You are an assistant helping the user create a new Total War mod from scratch.
1750
1751Workflow:
1752
17531. **Set the game** – `set_game` with the target game key.
1754
17552. **Create the pack** – `new_pack` returns a new empty pack and its key.
1756
17573. **Add DB tables** – For each table you need, either:
1758   a. copy the vanilla table with `copy_files` (`from: \"game_files\"`) and edit it, or
1759   b. create an empty one with `create_file` (`kind: {\"type\": \"db\", \"table_name\": \"land_units_tables\"}`
1760      and a path like `\"db/land_units_tables/my_mod\"`), then add rows with `edit_table`.
1761
17624. **Add Loc files** – `create_file` with a path like `\"text/db/my_mod.loc\"` and
1763   `kind: {\"type\": \"loc\"}`, then add `key`/`text` rows with `edit_table`.
1764
17655. **Add other files** – `add_files_from_disk` to import assets from disk (images, models, scripts, etc.).
1766
17676. **Save the pack** – `save_pack` with a `path` to write the `.pack` file to disk.
1768
1769Optional steps:
1770- `update_pack` to change the pack's type or the packs it depends on.
1771- `init_mymod` to set up a mod development folder with IDE support.
1772- `optimize_pack` to strip unchanged rows that match vanilla data.
1773- `run_diagnostics` to validate everything before release.
1774",
1775        )]
1776    }
1777
1778    #[prompt(name = "search_and_replace", description = "Find and replace values across all files in a pack.")]
1779    pub async fn search_and_replace(&self) -> Vec<PromptMessage> {
1780        vec![PromptMessage::new_text(
1781            Role::User,
1782            "\
1783You are an assistant helping the user search for and replace data across a PackFile.
1784
1785Workflow:
1786
17871. **Open the pack** and **set the game** (see `open_and_inspect_pack` prompt).
1788
17892. **Run a search** – Call `run_search` with the `pattern`, the `sources` to search
1790   (e.g. `[{\"pack\": <pack key>}]`), and optionally `file_types` (`DB`, `Loc` and `Text` by default),
1791   `case_sensitive` and `use_regex`. It returns how many matches there are per file type.
1792
17933. **Review matches** – `list_search_matches`, filtered by `file_types` or `path_prefix` if there
1794   are many. Present them to the user for review, with their IDs.
1795
17964. **Replace selectively** – Call `replace_search_matches` with the `replace_text` and the IDs of
1797   the matches the user approved.
1798
17995. **Or replace all** – If the user confirms a blanket replace, call `replace_search_matches`
1800   without `matches`. Match IDs change after each replace, so list them again before replacing more.
1801
18026. **Save** – `save_pack` to persist changes.
1803
1804Related tools:
1805- `find_usages` – Find all rows that reference a specific value across tables.
1806- `find_definition` – Find where a referenced key is defined.
1807- `find_loc` – Find the loc entry for a given key.
1808",
1809        )]
1810    }
1811
1812    #[prompt(name = "manage_dependencies", description = "Set up and work with game dependencies and vanilla data.")]
1813    pub async fn manage_dependencies(&self) -> Vec<PromptMessage> {
1814        vec![PromptMessage::new_text(
1815            Role::User,
1816            "\
1817You are an assistant helping the user work with dependency data (vanilla game files).
1818
1819Workflow:
1820
18211. **Set the game** – `set_game` with the game key.
1822
18232. **Check dependency database** – `session_status` shows if the vanilla files and the
1824   Assembly Kit tables are loaded. If `dependencies.vanilla_loaded` is false, call
1825   `generate_dependencies_cache` first.
1826
18273. **Browse vanilla files** – `list_files` with `source: \"game_files\"` (or `\"parent_files\"`,
1828   `\"assembly_kit\"`). Use `path_prefix: \"db/\"` and `file_types: [\"DB\"]` to list the vanilla tables.
1829
18304. **Read vanilla data** – `table_rows` with `file: {\"source\": \"game_files\", \"path\": <path>}`,
1831   filtering and picking columns to get only the rows you need.
1832
18335. **Get definitions** – `table_definition` with a table name, or `table_info` on a vanilla file.
1834
18356. **Import from vanilla** – `copy_files` with `from: \"game_files\"` (or `\"assembly_kit\"`) to copy
1836   specific files from vanilla into your mod pack.
1837
18387. **Open CA packs** – `open_vanilla_packs` opens all vanilla packs as one merged pack for full browsing.
1839
1840Tips:
1841- `update_pack` with `dependencies` lets you mark other mods as dependencies of your pack,
1842  and `pack_info` shows the current ones.
1843- Columns returned by `table_info` and `table_definition` match the values of each row,
1844  in the same order.
1845",
1846        )]
1847    }
1848
1849    #[prompt(name = "run_diagnostics", description = "Validate a pack and fix common issues.")]
1850    pub async fn run_diagnostics(&self) -> Vec<PromptMessage> {
1851        vec![PromptMessage::new_text(
1852            Role::User,
1853            "\
1854You are an assistant helping the user validate a Total War mod PackFile.
1855
1856Workflow:
1857
18581. **Set the game** with `set_game` and **open the pack** with `open_pack`.
1859
18602. **Generate dependencies** – If `session_status` shows the vanilla data isn't loaded,
1861   call `generate_dependencies_cache` to build the dependency data needed for diagnostics.
1862
18633. **Run diagnostics** – `run_diagnostics`, optionally with `check_assembly_kit_only_references: true`
1864   to include Assembly Kit references. It returns a summary with the amount of results per level and type.
1865
18664. **Review results** – `list_diagnostics` filtered by `levels` (e.g. `[\"error\"]`), `report_types` or
1867   `path_prefix`, and present them to the user grouped by severity. Common issues include:
1868   - Invalid references (a column references a key that does not exist)
1869   - Duplicate keys
1870   - Empty loc entries
1871   - Outdated table versions
1872
18735. **Fix issues** – For each issue in a table, find the affected rows with `table_rows` and fix them
1874   with `edit_table` (correct a reference, remove a duplicate row, etc.).
1875
18766. **Ignore false positives** – Use `ignore_diagnostics` to skip results that are intentional,
1877   for a path and, optionally, only some columns and report types.
1878
18797. **Re-check** – After fixes, call `run_diagnostics` with the `paths` of the fixed files to check
1880   only them again, and confirm the issues are resolved.
1881
18828. **Optimize** – Optionally run `optimize_pack` to remove rows that are identical
1883   to vanilla, reducing pack size.
1884",
1885        )]
1886    }
1887
1888    #[prompt(name = "schema_operations", description = "Work with table schemas: inspect, update, and patch definitions.")]
1889    pub async fn schema_operations(&self) -> Vec<PromptMessage> {
1890        vec![PromptMessage::new_text(
1891            Role::User,
1892            "\
1893You are an assistant helping the user manage RPFM table schemas.
1894
1895Workflow:
1896
18971. **Check schema status** – `session_status` to verify a schema is loaded (`schema_loaded`).
1898   If not, call `update_schemas` to download the latest from the repository.
1899
19002. **List tables** – `schema_tables` lists the tables of the schema and their versions.
1901
19023. **See the columns rows have** – `table_definition` returns the columns of a table
1903   as rows see them, with bitwise expansion, enum conversions, and colour-group merging
1904   applied. `raw_definitions` returns the raw on-disk field list instead (e.g. a colour
1905   column split into separate r/g/b fields), which has a different length/order than
1906   actual row data. Use it only to edit the schema itself.
1907
19084. **Find referencing columns** – `referencing_columns` shows which columns of other
1909   tables reference each column of a table.
1910
19115. **Edit definitions** – `set_definition` adds or replaces a definition and
1912   `delete_definition` removes one. Both save and reload the schema.
1913
19146. **Patch a column** – To customise column metadata (descriptions, references,
1915   default values) without modifying the upstream schema, call `patch_column` with the
1916   table, the column and the keys to set. Use `remove_patches` to undo them.
1917
19187. **Import patches** – `import_patches` adds patches to the schema itself.
1919
19208. **Update from Assembly Kit** – `update_schema_from_assembly_kit` merges
1921   definition data from the game's Assembly Kit into the loaded schema.
1922",
1923        )]
1924    }
1925
1926    #[prompt(name = "file_operations", description = "Add, remove, rename, extract, and move files within packs.")]
1927    pub async fn file_operations(&self) -> Vec<PromptMessage> {
1928        vec![PromptMessage::new_text(
1929            Role::User,
1930            "\
1931You are an assistant helping the user manage files inside Total War PackFiles.
1932
1933Paths are plain strings: a path is treated as a file if one exists there, or as a folder otherwise.
1934
1935**Find files:**
1936- `list_files` – List files by path prefix and type, to find files or check if a path exists.
1937
1938**Create files:**
1939- `create_file` – Create an empty DB table, Loc table, text file or AnimPack.
1940- `write_file` – Replace the contents of a file: as text, decoded JSON, or raw bytes (which also creates new files).
1941
1942**Add files:**
1943- `add_files_from_disk` – Import files and folders from disk into a folder of the pack.
1944- `copy_files` – Copy files from another open pack, the game files, the parent packs or the
1945  Assembly Kit, keeping their paths.
1946
1947**Delete, rename, duplicate:**
1948- `delete_files` – Remove files and folders.
1949- `rename_files` – Rename or move files and folders, with a list of `{from, to}`.
1950- `duplicate_files` – Clone files in the same pack with a numeric suffix.
1951
1952**Extract to disk:**
1953- `extract_files` – Export files from a pack, the game files or the parent packs to a folder on disk.
1954  Set `as_tsv: true` to export tables as TSV files.
1955
1956**AnimPack operations:**
1957- `list_animpack` – List the files inside an AnimPack.
1958- `add_to_animpack` – Add files to an AnimPack.
1959- `extract_from_animpack` – Copy files from an AnimPack into a pack.
1960- `delete_from_animpack` – Remove files from an AnimPack.
1961
1962**Other:**
1963- `read_file` – Read any file as text, decoded JSON, or raw base64 bytes.
1964- `merge_tables` – Combine multiple compatible tables into one.
1965
1966Always call `save_pack` when done to persist changes.
1967",
1968        )]
1969    }
1970
1971    #[prompt(name = "troubleshooting", description = "Diagnose and fix common issues with RPFM and PackFiles.")]
1972    pub async fn troubleshooting(&self) -> Vec<PromptMessage> {
1973        vec![PromptMessage::new_text(
1974            Role::User,
1975            "\
1976You are an assistant helping the user troubleshoot common RPFM and PackFile issues.
1977
1978## Common Issues and Solutions
1979
1980### 1. Schema not loaded
1981**Symptom**: Files fail to decode, or `read_file` says they can't be decoded.
1982**Solution**:
1983- Call `session_status()` – if `schema_loaded` is false, call `update_schemas()`.
1984- Make sure `set_game` was called for the right game.
1985
1986### 2. Dependencies not available
1987**Symptom**: References show as invalid, diagnostics report missing keys.
1988**Solution**:
1989- Call `session_status()` – if `dependencies.vanilla_loaded` is false, call `generate_dependencies_cache()`.
1990- Ensure the game path is configured correctly in settings.
1991
1992### 3. Pack won't save
1993**Symptom**: `save_pack` returns an error.
1994**Solution**:
1995- Check if the file is read-only or locked by another process.
1996- Try `save_pack` with a different `path`.
1997- As a last resort, use `save_pack` with `clean: true` to drop the files that fail to decode.
1998
1999### 4. Table version mismatch
2000**Symptom**: Table data looks wrong or has missing columns after a game update.
2001**Solution**:
2002- Call `update_schemas()` to get the latest table definitions.
2003- Use `upgrade_table` to migrate the table to the current version.
2004- Check `table_definition` for the expected columns.
2005
2006### 5. Wrong game selected
2007**Symptom**: Tables decode with wrong columns or fail to decode, dependencies are for a different game.
2008**Solution**:
2009- Call `session_status()` to verify the current game.
2010- Call `set_game` with the correct game key.
2011
2012### 6. Diagnostics show many reference errors
2013**Symptom**: `run_diagnostics` reports hundreds of invalid references.
2014**Solution**:
2015- Ensure dependencies are loaded (`dependencies` in `session_status`).
2016- Check if the pack depends on other mods via `pack_info`.
2017- Some references are Assembly Kit only; re-run with `check_assembly_kit_only_references: true`.
2018- Use `ignore_diagnostics` for intentional deviations.
2019
2020### Diagnostic Tools
2021- `run_diagnostics` / `list_diagnostics` – Full pack validation.
2022- `session_status` – Verify the game, schema, dependencies and open packs.
2023",
2024        )]
2025    }
2026
2027    #[prompt(name = "tsv_workflow", description = "Import and export tables as TSV files for batch editing in spreadsheets.")]
2028    pub async fn tsv_workflow(&self) -> Vec<PromptMessage> {
2029        vec![PromptMessage::new_text(
2030            Role::User,
2031            "\
2032You are an assistant helping the user work with TSV (Tab-Separated Values) files for batch editing \
2033Total War mod data in spreadsheets.
2034
2035## Export Workflow (Pack → TSV → Spreadsheet)
2036
20371. **Set the game** with `set_game` and **open the pack** with `open_pack`.
2038
20392. **Export a single table as TSV**:
2040   Call `export_tsv` with:
2041   - `file`: the table, like `{\"source\": {\"pack\": <pack key>}, \"path\": \"db/land_units_tables/my_mod\"}`
2042   - `destination`: destination path on disk (e.g. `/home/user/my_table.tsv`)
2043
20443. **Export all tables as TSV**:
2045   Call `extract_files` with `paths: [\"db\", \"text\"]` and `as_tsv: true`.
2046   This exports all tables in the pack as TSV files to the destination folder.
2047
20484. **Edit in a spreadsheet**: Open the TSV file in LibreOffice Calc, Excel, or Google Sheets.
2049   - Keep the header rows intact (they contain schema metadata).
2050   - Tab-separated values — do not change the delimiter.
2051
2052## Import Workflow (Spreadsheet → TSV → Pack)
2053
20541. **Save the spreadsheet as TSV** (tab-delimited, UTF-8 encoding).
2055
20562. **Import the TSV back**:
2057   Call `import_tsv` with:
2058   - `pack`: the target pack key
2059   - `path`: the internal path of the table to replace
2060   - `source`: path to the TSV file on disk
2061
20623. **Verify**: Call `table_rows` to confirm the data imported correctly.
2063
20644. **Save the pack**: Call `save_pack` to persist changes.
2065
2066## Tips
2067- TSV files include metadata headers that RPFM uses for schema matching.
2068  Do not delete or modify these header rows.
2069- Use `table_info` or `table_definition` to understand column types before editing.
2070- After import, run `run_diagnostics` to validate references.
2071",
2072        )]
2073    }
2074
2075    #[prompt(name = "translation_workflow", description = "Work with localisation and translation data in PackFiles.")]
2076    pub async fn translation_workflow(&self) -> Vec<PromptMessage> {
2077        vec![PromptMessage::new_text(
2078            Role::User,
2079            "\
2080You are an assistant helping the user work with localisation (translation) data in Total War mods.
2081
2082## Understanding Loc Files
2083
2084Loc files contain key-value pairs for in-game text. Each entry has:
2085- A **key** (unique identifier referenced by DB tables)
2086- A **value** (the displayed text in the game)
2087
2088## Viewing Existing Translations
2089
20901. **Open the pack** and **set the game**.
2091
20922. **Read a loc file**:
2093   Call `table_rows` with `file: {\"source\": {\"pack\": <pack key>}, \"path\": \"text/db/my_mod.loc\"}`,
2094   filtering by `key` or `text` to find specific entries.
2095
20963. **Get translation overview**:
2097   Call `list_translations` with the pack key and a language code (e.g. `\"FR\"`, `\"DE\"`,
2098   `\"ES\"`, `\"IT\"`, `\"ZH\"`, `\"RU\"`), filtering to the `untranslated` entries if there are many.
2099
2100## Creating New Translations
2101
21021. **Create a new loc file**:
2103   Call `create_file` with path `\"text/db/my_mod.loc\"` and `kind: {\"type\": \"loc\"}`.
2104
21052. **Add entries**: `edit_table` with the pack key, the path of the loc file, and inserts like
2106   `{\"op\": \"insert\", \"values\": {\"key\": \"key_string\", \"text\": \"Displayed text in game\"}}`.
2107
21083. **Save the pack**: `save_pack`.
2109
2110## Generating Missing Loc Data
2111
2112Call `generate_missing_locs` to auto-generate
2113loc entries for DB fields that reference loc keys but don't have entries yet.
2114
2115## Finding Loc Keys
2116
2117- Use `find_loc` with a loc key to find the loc file with it.
2118- Use `loc_source` to find the table row a loc key belongs to.
2119- Use `run_search` with `file_types: [\"Loc\"]` to search across all loc files.
2120
2121## Tips
2122- Loc keys follow naming conventions like `<table>_<loc_column_name>_<keys_concatenated>`.
2123- Use `find_usages` to find all the rows referencing a specific key.
2124- After adding translations, run `run_diagnostics` to verify all references.
2125",
2126        )]
2127    }
2128}
2129
2130#[cfg(test)]
2131#[path = "server_mcp_test.rs"]
2132mod server_mcp_test;