1use 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
87const MCP_JOB_WAIT: Duration = Duration::from_secs(45);
95
96const MCP_CACHE_TTL_MS: u64 = 60 * 60 * 1000;
100
101fn 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
113async 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
130fn error_result(error: &RpcError) -> CallToolResult {
132 CallToolResult::error(vec![ContentBlock::text(serde_json::to_string(error).unwrap_or_else(|_| error.message.clone()))])
133}
134
135const PARAMS_ERROR_PREFIX: &str = "failed to deserialize parameters: ";
137
138fn 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
150async 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 ¤t_stage {
183 Some(stage) => notification.with_message(stage),
184 None => notification,
185 };
186
187 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
197fn task_id(session: SessionId, job: u64) -> String {
199 format!("{session}-{job}")
200}
201
202fn 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
208fn unknown_task(task_id: &str) -> McpError {
210 McpError::invalid_params(format!("Unknown task: {task_id}"), None)
211}
212
213fn timestamp(time: SystemTime) -> String {
215 OffsetDateTime::from(time).format(&Rfc3339).unwrap_or_default()
216}
217
218fn 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
241fn 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#[derive(Clone)]
262pub struct McpServer {
263 sessions: Arc<SessionManager>,
265 tool_router: ToolRouter<Self>,
267 prompt_router: PromptRouter<Self>,
269}
270
271pub struct JobToolResponse(CallToolResponse);
276
277impl 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 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 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 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 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 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 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 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 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 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 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#[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;