Skip to main content

rpfm_server/
server_mcp.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! [Model Context Protocol][mcp] server exposed at the `/mcp` endpoint.
12//!
13//! Wraps every [`Command`] the [`crate::background_thread`] dispatcher
14//! understands as an MCP **tool**, plus a handful of MCP **resources**
15//! (game lists, enum dumps, examples, reference docs) and **prompts** for
16//! common workflows ("open and inspect a pack", "edit a DB table",
17//! "manage dependencies", …). Each MCP client gets its own dedicated
18//! [`Session`] and [`McpServer`] — same isolation guarantees as the
19//! WebSocket clients.
20//!
21//! Each tool call:
22//!
23//! 1. Translates its `*Args` payload into a [`Command`] and ships it
24//!    through the session's
25//!    [`background_loop`](crate::background_thread::background_loop) via
26//!    the `send_and_respond!` helper.
27//! 2. Wraps the resulting [`Response`] back into a [`CallToolResult`].
28//!
29//! The `*Args` structs are the canonical schema for every tool. Their
30//! `JsonSchema` derive is what `rmcp` ships to clients to advertise tool
31//! arguments, so docstrings on individual fields show up directly in MCP
32//! tool listings.
33//!
34//! [mcp]: https://modelcontextprotocol.io/
35//! [`Session`]: crate::session::Session
36//! [`CallToolResult`]: rmcp::model::CallToolResult
37
38use rmcp::ErrorData as McpError;
39use rmcp::handler::server::{router::prompt::PromptRouter, tool::ToolRouter, wrapper::Parameters};
40use rmcp::model::{
41    CallToolResult, CompletionInfo, CompleteRequestParams, CompleteResult,
42    ContentBlock, ErrorCode, ListResourcesResult, ListResourceTemplatesResult,
43    PaginatedRequestParams, PromptMessage,
44    ReadResourceRequestParams, ReadResourceResult, ReadResourceResponse,
45    Resource, ResourceContents, Role, ServerCapabilities, ServerInfo,
46};
47use rmcp::service::RequestContext;
48use rmcp::{prompt, prompt_handler, prompt_router, tool, tool_handler, tool_router, RoleServer};
49use schemars::JsonSchema;
50use serde::{Deserialize, Serialize};
51
52use std::collections::{BTreeMap, HashMap, HashSet};
53use std::sync::Arc;
54use std::path::PathBuf;
55
56use rpfm_extensions::merge::MergeOptions;
57use rpfm_extensions::translator::DEFAULT_SRC_LANG;
58
59use rpfm_ipc::helpers::DataSource;
60use rpfm_ipc::messages::{Command, Response};
61use rpfm_lib::files::{ContainerPath, RFile, RFileDecoded};
62use rpfm_telemetry::sentry;
63
64use crate::session::{Session, recv_response};
65
66//-------------------------------------------------------------------------------//
67//                              Helper macro
68//-------------------------------------------------------------------------------//
69
70/// Helper to send a command and return the JSON response.
71///
72/// Each tool call starts an independent Sentry transaction following the MCP tracing spec,
73/// so it gets reported regardless of the long-lived rmcp service span.
74macro_rules! send_and_respond {
75    ($self:expr, $tool_name:expr, $cmd:expr) => {{
76        let tx_ctx = sentry::TransactionContext::new(
77            &format!("tools/call {}", $tool_name),
78            "mcp.server",
79        );
80        let tx = sentry::start_transaction(tx_ctx);
81        tx.set_data("mcp.method.name", sentry::protocol::Value::from("tools/call"));
82        tx.set_data("mcp.tool.name", sentry::protocol::Value::from($tool_name));
83        tx.set_data("mcp.transport", sentry::protocol::Value::from("streamable-http"));
84
85        sentry::configure_scope(|scope| scope.set_span(Some(tx.clone().into())));
86
87        let mut receiver = $self.session.send($cmd);
88        let response = recv_response(&mut receiver).await;
89
90        tx.finish();
91
92        let is_error = matches!(&response, Response::Error(_));
93
94        let json = serde_json::to_string(&response).map_err(|e| McpError {
95            code: ErrorCode::INTERNAL_ERROR,
96            message: format!("Failed to serialize response: {e}").into(),
97            data: None,
98        })?;
99
100        if is_error {
101            Ok(CallToolResult::error(vec![ContentBlock::text(json)]))
102        } else {
103            Ok(CallToolResult::success(vec![ContentBlock::text(json)]))
104        }
105    }};
106}
107
108/// Build a `Resource` with common fields set.
109fn resource(uri: &str, name: &str, description: &str, mime_type: &str) -> Resource {
110    Resource::new(uri, name)
111        .with_description(description)
112        .with_mime_type(mime_type)
113}
114
115/// Parse a JSON string into the expected type, returning a tool-level error on failure.
116///
117/// This is a macro (not a function) so that `return Ok(...)` exits the calling tool method,
118/// keeping invalid-JSON errors as tool results instead of protocol-level `McpError`s that
119/// would tear down the MCP session.
120macro_rules! parse_json {
121    ($input:expr) => {
122        match serde_json::from_str($input) {
123            Ok(v) => v,
124            Err(e) => return Ok(CallToolResult::error(vec![ContentBlock::text(format!("Invalid JSON parameter: {e}"))])),
125        }
126    };
127}
128
129//-------------------------------------------------------------------------------//
130//                              Enums & Structs
131//-------------------------------------------------------------------------------//
132
133/// MCP server bound to a single [`Session`].
134///
135/// One instance is constructed per MCP client connection by the
136/// `StreamableHttpService` factory wired in `main.rs`. The
137/// `tool_router` and `prompt_router` fields are built once at construction
138/// time from the `#[tool_router]` / `#[prompt_router]` attribute macros
139/// applied further down in this module.
140///
141/// Cheap to clone — only `Arc` and small router structs.
142#[derive(Clone)]
143pub struct McpServer {
144    /// The session this MCP client is bound to.
145    session: Arc<Session>,
146    /// The router auto-generated from `#[tool_router]` annotations.
147    tool_router: ToolRouter<Self>,
148    /// The router auto-generated from `#[prompt_router]` annotations.
149    prompt_router: PromptRouter<Self>,
150}
151
152// -- Generic / Existing Args --
153
154#[derive(Debug, Deserialize, JsonSchema, Serialize)]
155#[schemars(description = "Call any IPC command directly.")]
156pub struct CallCommandArgs {
157    /// The JSON representation of the Command enum.
158    pub command: String,
159}
160
161#[derive(Debug, Deserialize, JsonSchema, Serialize)]
162pub struct OpenPackfilesArgs {
163    /// The paths of the PackFiles to open.
164    pub paths: Vec<PathBuf>,
165}
166
167#[derive(Debug, Deserialize, JsonSchema, Serialize)]
168pub struct SetGameSelectedArgs {
169    /// The name of the game to select.
170    pub game_name: String,
171    /// Whether to rebuild dependencies.
172    pub rebuild_dependencies: bool,
173}
174
175#[derive(Debug, Deserialize, JsonSchema, Serialize)]
176pub struct TsvExportArgs {
177    /// The key of the target pack.
178    pub pack_key: String,
179    /// The path of the TSV file to export to.
180    pub tsv_path: PathBuf,
181    /// The path of the table to export.
182    pub table_path: String,
183}
184
185#[derive(Debug, Deserialize, JsonSchema, Serialize)]
186pub struct TsvImportArgs {
187    /// The key of the target pack.
188    pub pack_key: String,
189    /// The path of the TSV file to import from.
190    pub tsv_path: PathBuf,
191    /// The path of the table to import to.
192    pub table_path: String,
193}
194
195#[derive(Debug, Deserialize, JsonSchema, Serialize)]
196pub struct DecodePackedFileArgs {
197    /// The key of the target pack.
198    pub pack_key: String,
199    /// The path of the file inside the data source.
200    pub path: String,
201    /// The data source to decode from.
202    pub source: DataSource,
203}
204
205// -- Pack Lifecycle Args --
206
207#[derive(Debug, Deserialize, JsonSchema, Serialize)]
208pub struct PathArg {
209    /// The file path.
210    pub path: PathBuf,
211}
212
213#[derive(Debug, Deserialize, JsonSchema, Serialize)]
214pub struct TableColumnArgs {
215    /// Name of the DB table, like `factions_tables`.
216    pub table_name: String,
217    /// Name of the column.
218    pub column_name: String,
219}
220
221// -- Pack Key Args (multi-pack support) --
222
223#[derive(Debug, Deserialize, JsonSchema, Serialize)]
224pub struct PackKeyArg {
225    /// The key of the target pack. Use `list_open_packs` to get available keys.
226    pub pack_key: String,
227}
228
229#[derive(Debug, Deserialize, JsonSchema, Serialize)]
230pub struct PackKeyBoolArg {
231    /// The key of the target pack.
232    pub pack_key: String,
233    /// A boolean value.
234    pub value: bool,
235}
236
237#[derive(Debug, Deserialize, JsonSchema, Serialize)]
238pub struct PackKeyStringArg {
239    /// The key of the target pack.
240    pub pack_key: String,
241    /// A string value.
242    pub value: String,
243}
244
245#[derive(Debug, Deserialize, JsonSchema, Serialize)]
246pub struct PackKeyStringsArg {
247    /// The key of the target pack.
248    pub pack_key: String,
249    /// A list of string values.
250    pub values: Vec<String>,
251}
252
253#[derive(Debug, Deserialize, JsonSchema, Serialize)]
254pub struct PackKeyPathArg {
255    /// The key of the target pack.
256    pub pack_key: String,
257    /// The file path.
258    pub path: PathBuf,
259}
260
261// -- Pack Metadata Args --
262
263#[derive(Debug, Deserialize, JsonSchema, Serialize)]
264pub struct SetPackFileTypeArgs {
265    /// The key of the target pack.
266    pub pack_key: String,
267    /// The JSON representation of the PFHFileType enum.
268    pub pack_file_type: String,
269}
270
271#[derive(Debug, Deserialize, JsonSchema, Serialize)]
272pub struct ChangeCompressionFormatArgs {
273    /// The key of the target pack.
274    pub pack_key: String,
275    /// The JSON representation of the CompressionFormat enum.
276    pub format: String,
277}
278
279#[derive(Debug, Deserialize, JsonSchema, Serialize)]
280pub struct BoolArg {
281    /// A boolean value.
282    pub value: bool,
283}
284
285#[derive(Debug, Deserialize, JsonSchema, Serialize)]
286pub struct SetPackSettingsArgs {
287    /// The key of the target pack.
288    pub pack_key: String,
289    /// The JSON representation of the PackSettings struct.
290    pub settings: String,
291}
292
293#[derive(Debug, Deserialize, JsonSchema, Serialize)]
294pub struct SetDependencyPackFilesListArgs {
295    /// The key of the target pack.
296    pub pack_key: String,
297    /// The JSON representation of Vec<(bool, String)> for the dependency list.
298    pub list: String,
299}
300
301// -- File Operations Args --
302
303#[derive(Debug, Deserialize, JsonSchema, Serialize)]
304pub struct NewPackedFileArgs {
305    /// The key of the target pack.
306    pub pack_key: String,
307    /// The path for the new file inside the pack.
308    pub path: String,
309    /// The JSON representation of the NewFile enum.
310    pub new_file: String,
311}
312
313#[derive(Debug, Deserialize, JsonSchema, Serialize)]
314pub struct AddPackedFilesArgs {
315    /// The key of the target pack.
316    pub pack_key: String,
317    /// The source filesystem paths.
318    pub source_paths: Vec<PathBuf>,
319    /// The JSON representation of Vec<ContainerPath> for destination paths.
320    pub destination_paths: String,
321    /// The optional paths to ignore (JSON representation of Option<Vec<PathBuf>>).
322    pub ignore_paths: Option<Vec<PathBuf>>,
323}
324
325#[derive(Debug, Deserialize, JsonSchema, Serialize)]
326pub struct AddPackedFilesFromPackFileArgs {
327    /// The key of the target pack.
328    pub pack_key: String,
329    /// The key of the source PackFile.
330    pub source_pack_path: String,
331    /// The JSON representation of Vec<ContainerPath> for files to add.
332    pub container_paths: String,
333}
334
335#[derive(Debug, Deserialize, JsonSchema, Serialize)]
336pub struct AddPackedFilesFromPackFileToAnimpackArgs {
337    /// The key of the source pack the files are copied from.
338    pub source_pack_key: String,
339    /// The key of the pack that owns the target AnimPack (may differ from the source).
340    pub pack_key: String,
341    /// The animpack path.
342    pub animpack_path: String,
343    /// The JSON representation of Vec<ContainerPath> for files to add.
344    pub container_paths: String,
345}
346
347#[derive(Debug, Deserialize, JsonSchema, Serialize)]
348pub struct AddPackedFilesFromAnimpackArgs {
349    /// The key of the pack that owns the AnimPack (only used when `source` is a PackFile).
350    pub anim_pack_key: String,
351    /// The key of the destination pack the files are copied into (may differ from the AnimPack's).
352    pub pack_key: String,
353    /// The data source to get the animpack from.
354    pub source: DataSource,
355    /// The animpack path.
356    pub animpack_path: String,
357    /// The JSON representation of Vec<ContainerPath> for files to add.
358    pub container_paths: String,
359}
360
361#[derive(Debug, Deserialize, JsonSchema, Serialize)]
362pub struct ContainerPathsArg {
363    /// The key of the target pack.
364    pub pack_key: String,
365    /// The JSON representation of Vec<ContainerPath>.
366    pub paths: String,
367}
368
369#[derive(Debug, Deserialize, JsonSchema, Serialize)]
370pub struct DeleteFromAnimpackArgs {
371    /// The key of the target pack.
372    pub pack_key: String,
373    /// The animpack path.
374    pub animpack_path: String,
375    /// The JSON representation of Vec<ContainerPath> for files to delete.
376    pub container_paths: String,
377}
378
379#[derive(Debug, Deserialize, JsonSchema, Serialize)]
380pub struct ExtractPackedFilesArgs {
381    /// The key of the target pack.
382    pub pack_key: String,
383    /// The JSON representation of BTreeMap<DataSource, Vec<ContainerPath>>.
384    pub source_paths: String,
385    /// The destination path on disk.
386    pub destination_path: PathBuf,
387    /// Whether to export tables as TSV.
388    pub export_as_tsv: bool,
389}
390
391#[derive(Debug, Deserialize, JsonSchema, Serialize)]
392pub struct RenamePackedFilesArgs {
393    /// The key of the target pack.
394    pub pack_key: String,
395    /// The JSON representation of Vec<(ContainerPath, ContainerPath)>.
396    pub renames: String,
397}
398
399#[derive(Debug, Deserialize, JsonSchema, Serialize)]
400pub struct CopyOrCutPackedFilesArgs {
401    /// A JSON object mapping pack key to ContainerPath arrays, e.g. {"my_pack.pack": [{"File": "db/table/file"}]}.
402    pub paths_by_pack: String,
403}
404
405#[derive(Debug, Deserialize, JsonSchema, Serialize)]
406pub struct PastePackedFilesArgs {
407    /// The key of the target pack to paste into.
408    pub pack_key: String,
409    /// The destination folder path inside the pack (use empty string for root).
410    pub destination_path: String,
411}
412
413#[derive(Debug, Deserialize, JsonSchema, Serialize)]
414pub struct DuplicatePackedFilesArgs {
415    /// The key of the target pack.
416    pub pack_key: String,
417    /// The JSON representation of Vec<ContainerPath> for files to duplicate.
418    pub paths: String,
419}
420
421#[derive(Debug, Deserialize, JsonSchema, Serialize)]
422pub struct SavePackedFileFromViewArgs {
423    /// The key of the target pack.
424    pub pack_key: String,
425    /// The path of the file inside the pack.
426    pub path: String,
427    /// The JSON representation of the RFileDecoded enum.
428    pub data: String,
429}
430
431#[derive(Debug, Deserialize, JsonSchema, Serialize)]
432pub struct SavePackedFileFromExternalViewArgs {
433    /// The key of the target pack.
434    pub pack_key: String,
435    /// The internal path of the file in the pack.
436    pub internal_path: String,
437    /// The external file path on disk.
438    pub external_path: PathBuf,
439}
440
441#[derive(Debug, Deserialize, JsonSchema, Serialize)]
442pub struct SavePackedFilesToPackFileAndCleanArgs {
443    /// The key of the target pack.
444    pub pack_key: String,
445    /// The JSON representation of Vec<RFile>.
446    pub files: String,
447    /// Whether to optimize after saving.
448    pub optimize: bool,
449}
450
451#[derive(Debug, Deserialize, JsonSchema, Serialize)]
452pub struct StringArg {
453    /// A string value.
454    pub value: String,
455}
456
457#[derive(Debug, Deserialize, JsonSchema, Serialize)]
458pub struct OpenPackedFileInExternalProgramArgs {
459    /// The key of the target pack.
460    pub pack_key: String,
461    /// The data source of the file.
462    pub source: DataSource,
463    /// The JSON representation of the ContainerPath.
464    pub container_path: String,
465}
466
467#[derive(Debug, Deserialize, JsonSchema, Serialize)]
468pub struct StringsArg {
469    /// A list of string values.
470    pub values: Vec<String>,
471}
472
473// -- Dependency Args --
474
475#[derive(Debug, Deserialize, JsonSchema, Serialize)]
476pub struct ImportDependenciesArgs {
477    /// The key of the target pack.
478    pub pack_key: String,
479    /// The JSON representation of BTreeMap<DataSource, Vec<ContainerPath>>.
480    pub paths: String,
481}
482
483#[derive(Debug, Deserialize, JsonSchema, Serialize)]
484pub struct GetRFilesFromAllSourcesArgs {
485    /// The JSON representation of Vec<ContainerPath>.
486    pub paths: String,
487    /// Whether to lowercase paths.
488    pub lowercase: bool,
489}
490
491#[derive(Debug, Deserialize, JsonSchema, Serialize)]
492pub struct ContainerPathArg {
493    /// The JSON representation of the ContainerPath.
494    pub path: String,
495}
496
497// -- Search Args --
498
499#[derive(Debug, Deserialize, JsonSchema, Serialize)]
500pub struct GlobalSearchArgs {
501    /// The key of the target pack.
502    pub pack_key: String,
503    /// The JSON representation of the GlobalSearch struct.
504    pub search: String,
505}
506
507#[derive(Debug, Deserialize, JsonSchema, Serialize)]
508pub struct GlobalSearchReplaceMatchesArgs {
509    /// The key of the target pack.
510    pub pack_key: String,
511    /// The JSON representation of the GlobalSearch struct.
512    pub search: String,
513    /// The JSON representation of Vec<MatchHolder>.
514    pub matches: String,
515}
516
517#[derive(Debug, Deserialize, JsonSchema, Serialize)]
518pub struct SearchReferencesArgs {
519    /// The key of the target pack.
520    pub pack_key: String,
521    /// The JSON representation of HashMap<String, Vec<String>>.
522    pub reference_map: String,
523    /// The value to search for.
524    pub value: String,
525}
526
527#[derive(Debug, Deserialize, JsonSchema, Serialize)]
528pub struct GetReferenceDataFromDefinitionArgs {
529    /// The key of the target pack.
530    pub pack_key: String,
531    /// The table name.
532    pub table_name: String,
533    /// The JSON representation of the Definition struct.
534    pub definition: String,
535    /// Force local reference regeneration.
536    pub force: bool,
537}
538
539#[derive(Debug, Deserialize, JsonSchema, Serialize)]
540pub struct GoToDefinitionArgs {
541    /// The key of the target pack.
542    pub pack_key: String,
543    /// The table name.
544    pub table_name: String,
545    /// The column name.
546    pub column_name: String,
547    /// The values to search for.
548    pub values: Vec<String>,
549}
550
551// -- Schema Args --
552
553#[derive(Debug, Deserialize, JsonSchema, Serialize)]
554pub struct SaveSchemaArgs {
555    /// The JSON representation of the Schema struct.
556    pub schema: String,
557}
558
559#[derive(Debug, Deserialize, JsonSchema, Serialize)]
560pub struct StringI32Args {
561    /// A string value (e.g., table name).
562    pub name: String,
563    /// An integer value (e.g., version).
564    pub version: i32,
565}
566
567#[derive(Debug, Deserialize, JsonSchema, Serialize)]
568pub struct ReferencingColumnsForDefinitionArgs {
569    /// The table name.
570    pub table_name: String,
571    /// The JSON representation of the Definition struct.
572    pub definition: String,
573}
574
575#[derive(Debug, Deserialize, JsonSchema, Serialize)]
576pub struct DefinitionArg {
577    /// The JSON representation of the Definition struct.
578    pub definition: String,
579}
580
581#[derive(Debug, Deserialize, JsonSchema, Serialize)]
582pub struct SchemaPatchArgs {
583    /// The JSON representation of HashMap<String, DefinitionPatch>.
584    pub patches: String,
585}
586
587// -- Table Ops Args --
588
589#[derive(Debug, Deserialize, JsonSchema, Serialize)]
590pub struct MergeFilesArgs {
591    /// The key of the target pack.
592    pub pack_key: String,
593    /// The JSON representation of Vec<ContainerPath> for files to merge.
594    pub paths: String,
595    /// The path for the merged file.
596    pub merged_path: String,
597    /// Whether to delete source files after merging.
598    pub delete_source: bool,
599    /// Merge rows by key instead of concatenating them. If some rows can't be reconciled
600    /// automatically, nothing is written and the response is `Response::MergeConflicts` instead.
601    /// Defaults to false.
602    #[serde(default)]
603    pub delta_merge: bool,
604}
605
606#[derive(Debug, Deserialize, JsonSchema, Serialize)]
607pub struct CascadeEditionArgs {
608    /// The key of the target pack.
609    pub pack_key: String,
610    /// The table name.
611    pub table_name: String,
612    /// The JSON representation of the Definition struct.
613    pub definition: String,
614    /// The JSON representation of Vec<(Field, String, String)> for field changes.
615    pub changes: String,
616}
617
618#[derive(Debug, Deserialize, JsonSchema, Serialize)]
619pub struct AddKeysToKeyDeletesArgs {
620    /// The key of the target pack.
621    pub pack_key: String,
622    /// The table file name.
623    pub table_file_name: String,
624    /// The key table name.
625    pub key_table_name: String,
626    /// The keys to add.
627    pub keys: HashSet<String>,
628}
629
630// -- Diagnostics Args --
631
632#[derive(Debug, Deserialize, JsonSchema, Serialize)]
633pub struct DiagnosticsCheckArgs {
634    /// The list of ignored diagnostics.
635    pub ignored: Vec<String>,
636    /// Whether to check AK-only references.
637    pub check_ak_only_refs: bool,
638}
639
640#[derive(Debug, Deserialize, JsonSchema, Serialize)]
641pub struct LuaRunTestsArgs {
642    /// Code of the Lua test file.
643    pub test_source: String,
644    /// Campaign whose vanilla scripts to load, like "main_warhammer". Omit it to load only the script libraries and the mods.
645    pub campaign: Option<String>,
646}
647
648#[derive(Debug, Deserialize, JsonSchema, Serialize)]
649pub struct DiagnosticsUpdateArgs {
650    /// The JSON representation of the Diagnostics struct.
651    pub diagnostics: String,
652    /// The JSON representation of Vec<ContainerPath> for paths to check.
653    pub paths: String,
654    /// Whether to check AK-only references.
655    pub check_ak_only_refs: bool,
656}
657
658// -- Notes Args --
659
660#[derive(Debug, Deserialize, JsonSchema, Serialize)]
661pub struct AddNoteArgs {
662    /// The key of the target pack.
663    pub pack_key: String,
664    /// The JSON representation of the Note struct.
665    pub note: String,
666}
667
668#[derive(Debug, Deserialize, JsonSchema, Serialize)]
669pub struct DeleteNoteArgs {
670    /// The key of the target pack.
671    pub pack_key: String,
672    /// The path the note belongs to.
673    pub path: String,
674    /// The note ID.
675    pub id: u64,
676}
677
678// -- Optimization Args --
679
680#[derive(Debug, Deserialize, JsonSchema, Serialize)]
681pub struct OptimizePackFileArgs {
682    /// The key of the target pack.
683    pub pack_key: String,
684    /// The JSON representation of the OptimizerOptions struct.
685    pub options: String,
686}
687
688// -- Settings Args --
689
690#[derive(Debug, Deserialize, JsonSchema, Serialize)]
691pub struct SettingsSetBoolArgs {
692    /// The setting key.
693    pub key: String,
694    /// The boolean value.
695    pub value: bool,
696}
697
698#[derive(Debug, Deserialize, JsonSchema, Serialize)]
699pub struct SettingsSetI32Args {
700    /// The setting key.
701    pub key: String,
702    /// The integer value.
703    pub value: i32,
704}
705
706#[derive(Debug, Deserialize, JsonSchema, Serialize)]
707pub struct SettingsSetF32Args {
708    /// The setting key.
709    pub key: String,
710    /// The float value.
711    pub value: f32,
712}
713
714#[derive(Debug, Deserialize, JsonSchema, Serialize)]
715pub struct SettingsSetStringArgs {
716    /// The setting key.
717    pub key: String,
718    /// The string value.
719    pub value: String,
720}
721
722#[derive(Debug, Deserialize, JsonSchema, Serialize)]
723pub struct SettingsSetPathBufArgs {
724    /// The setting key.
725    pub key: String,
726    /// The path value.
727    pub value: PathBuf,
728}
729
730#[derive(Debug, Deserialize, JsonSchema, Serialize)]
731pub struct SettingsSetVecStringArgs {
732    /// The setting key.
733    pub key: String,
734    /// The list of string values.
735    pub value: Vec<String>,
736}
737
738#[derive(Debug, Deserialize, JsonSchema, Serialize)]
739pub struct SettingsSetVecRawArgs {
740    /// The setting key.
741    pub key: String,
742    /// The raw byte values.
743    pub value: Vec<u8>,
744}
745
746// -- Specialized Args --
747
748#[derive(Debug, Deserialize, JsonSchema, Serialize)]
749pub struct InitializeMyModFolderArgs {
750    /// The mod name.
751    pub name: String,
752    /// The game key.
753    pub game: String,
754    /// Whether to add Sublime Text support.
755    pub sublime: bool,
756    /// Whether to add VS Code support.
757    pub vscode: bool,
758    /// Optional gitignore template content.
759    pub gitignore: Option<String>,
760}
761
762#[derive(Debug, Deserialize, JsonSchema, Serialize)]
763pub struct PackMapArgs {
764    /// The key of the target pack.
765    pub pack_key: String,
766    /// The tile map paths.
767    pub tile_maps: Vec<PathBuf>,
768    /// The JSON representation of Vec<(PathBuf, String)> for tile path/name pairs.
769    pub tiles: String,
770}
771
772#[derive(Debug, Deserialize, JsonSchema, Serialize)]
773pub struct BuildStarposArgs {
774    /// The key of the target pack.
775    pub pack_key: String,
776    /// The campaign ID.
777    pub campaign_id: String,
778    /// Whether to process HLP/SPD data.
779    pub process_hlp_spd: bool,
780}
781
782#[derive(Debug, Deserialize, JsonSchema, Serialize)]
783pub struct UpdateAnimIdsArgs {
784    /// The key of the target pack.
785    pub pack_key: String,
786    /// The starting animation ID.
787    pub starting_id: i32,
788    /// The offset to apply.
789    pub offset: i32,
790}
791
792#[derive(Debug, Deserialize, JsonSchema, Serialize)]
793pub struct ExportRigidToGltfArgs {
794    /// The JSON representation of the RigidModel struct.
795    pub rigid_model: String,
796    /// The output path.
797    pub output_path: String,
798}
799
800#[derive(Debug, Deserialize, JsonSchema, Serialize)]
801pub struct SetVideoFormatArgs {
802    /// The key of the target pack.
803    pub pack_key: String,
804    /// The path of the video file in the pack.
805    pub path: String,
806    /// The JSON representation of the SupportedFormats enum.
807    pub format: String,
808}
809
810#[derive(Debug, Deserialize, JsonSchema, Serialize)]
811pub struct GetPackTranslationArgs {
812    /// The key of the target pack.
813    pub pack_key: String,
814    /// The source language code these translations are based on (e.g. "EN").
815    #[serde(default = "default_src_lang")]
816    pub src_lang: String,
817    /// The target language code.
818    pub language: String,
819}
820
821#[derive(Debug, Deserialize, JsonSchema, Serialize)]
822pub struct SrcLangArg {
823    /// The source language code (e.g. "SP").
824    pub src_lang: String,
825}
826
827fn default_src_lang() -> String {
828    DEFAULT_SRC_LANG.to_owned()
829}
830
831//-------------------------------------------------------------------------------//
832//                             Implementations
833//-------------------------------------------------------------------------------//
834
835#[tool_handler(router = self.tool_router)]
836#[prompt_handler(router = self.prompt_router)]
837impl rmcp::ServerHandler for McpServer {
838    fn get_info(&self) -> ServerInfo {
839        let capabilities = ServerCapabilities::builder()
840            .enable_tools()
841            .enable_prompts()
842            .enable_resources()
843            .enable_completions()
844            .build();
845
846        // `ServerInfo` is `#[non_exhaustive]` in rmcp, so it must be built through its constructor instead of a struct literal.
847        ServerInfo::new(capabilities).with_instructions("\
848This is the MCP server for RPFM (Rusted PackFile Manager), a tool for modding Total War games by \
849Creative Assembly. It lets you read, edit, create, and manage PackFiles (.pack) — the archive \
850format used by all modern Total War titles.
851
852## Key Concepts
853
854- **PackFile**: An archive containing game data files (DB tables, localisation, textures, models, etc.). \
855  Mods are distributed as PackFiles.
856- **pack_key**: When you open one or more PackFiles, each gets a unique key string. Use `list_open_packs` \
857  to discover available keys. Most tools require a `pack_key` parameter.
858- **DataSource**: Where data lives — `\"PackFile\"` (the user's mod), `\"GameFiles\"` (vanilla game data), \
859  `\"ParentFiles\"` (dependency mods), `\"AssKitFiles\"` (Assembly Kit data), `\"ExternalFile\"` (disk file).
860- **ContainerPath**: A path inside a pack — either `{\"File\": \"db/land_units_tables/my_table\"}` or \
861  `{\"Folder\": \"db/land_units_tables\"}`. Use an empty string for root folder.
862
863## Required Initialization Sequence
864
8651. **Set the game** — Call `set_game_selected` with the game key (e.g. `\"warhammer_3\"`) and \
866   `rebuild_dependencies: true`. This loads schemas and vanilla data.
8672. **Open a pack** — Call `open_packfiles` with filesystem path(s). Note the returned pack key(s).
8683. **Verify schema** — Call `is_schema_loaded`; if false, call `update_schemas` first.
869
870## Supported Games
871
872Valid game keys: `pharaoh_dynasties`, `pharaoh`, `warhammer_3`, `troy`, `three_kingdoms`, \
873`warhammer_2`, `warhammer`, `thrones_of_britannia`, `attila`, `rome_2`, `shogun_2`, `napoleon`, \
874`empire`, `arena`.
875
876## Common File Path Conventions
877
878- DB tables: `db/<table_name>/<file_name>` (e.g. `db/land_units_tables/my_mod`)
879- Localisation: `text/db/<file_name>.loc`
880- Scripts: `script/<path>.lua`
881- Images: `ui/<path>.png`
882
883## Pack File Types (PFHFileType)
884
885`\"Boot\"`, `\"Release\"`, `\"Patch\"`, `\"Mod\"` (default for mods), `\"Movie\"`.
886
887## Compression Formats
888
889`\"None\"` (default), `\"Lzma1\"` (legacy), `\"Lz4\"` (WH3 6.2+), `\"Zstd\"` (WH3 6.2+).
890
891## Creating New Files (NewFile)
892
893- DB table: `{\"DB\": [\"file_name\", \"table_name\", version]}` — e.g. `{\"DB\": [\"my_mod\", \"land_units_tables\", 0]}`
894- Loc file: `{\"Loc\": \"file_name\"}`
895- Text file: `{\"Text\": [\"file_name\", \"Plain\"]}` — formats: `\"Plain\"`, `\"Html\"`, `\"Xml\"`, `\"Lua\"`, `\"Cpp\"`, `\"Json\"`, `\"Markdown\"`, `\"Smithy\"`
896- AnimPack: `{\"AnimPack\": \"file_name\"}`
897- PortraitSettings: `{\"PortraitSettings\": [\"file_name\", version, [[\"entry_key\", \"entry_value\"]]]}`
898- VMD: `{\"VMD\": \"file_name\"}`
899- WSModel: `{\"WSModel\": \"file_name\"}`
900
901## Resources
902
903Use `resources/list` and `resources/read` to browse reference data: valid enum values, game lists, \
904and example JSON payloads without needing tool calls.
905
906## Responses
907
908All tool responses are JSON-serialized. On failure, an error message is returned instead of the expected data.
909")
910    }
911
912    //-----------------------------------------------------------------------//
913    // Resources
914    //-----------------------------------------------------------------------//
915
916    async fn list_resources(
917        &self,
918        _request: Option<PaginatedRequestParams>,
919        _context: RequestContext<RoleServer>,
920    ) -> Result<ListResourcesResult, McpError> {
921        let resources = vec![
922            resource("rpfm://games", "games", "List of all supported Total War game keys.", "application/json"),
923            resource("rpfm://enums/PFHFileType", "PFHFileType", "Valid PackFile type values (Boot, Release, Patch, Mod, Movie).", "application/json"),
924            resource("rpfm://enums/CompressionFormat", "CompressionFormat", "Valid compression format values (None, Lzma1, Lz4, Zstd).", "application/json"),
925            resource("rpfm://enums/DataSource", "DataSource", "Valid data source values indicating where data comes from.", "application/json"),
926            resource("rpfm://enums/ContainerPath", "ContainerPath", "ContainerPath enum variants with JSON examples.", "application/json"),
927            resource("rpfm://enums/NewFile", "NewFile", "NewFile enum variants for creating files inside packs, with JSON examples.", "application/json"),
928            resource("rpfm://enums/SupportedFormats", "SupportedFormats", "Valid video format values (CaVp8, Ivf).", "application/json"),
929            resource("rpfm://examples/global_search", "GlobalSearch example", "Example JSON for the GlobalSearch struct used by search tools.", "application/json"),
930            resource("rpfm://examples/optimizer_options", "OptimizerOptions example", "Example JSON for OptimizerOptions with all boolean fields.", "application/json"),
931            resource("rpfm://reference/initialization", "Initialization guide", "Step-by-step guide for initializing the RPFM MCP server session.", "text/plain"),
932            resource("rpfm://reference/path_conventions", "Path conventions", "Common file path conventions inside Total War PackFiles.", "text/plain"),
933        ];
934        Ok(ListResourcesResult {
935            resources,
936            ..Default::default()
937        })
938    }
939
940    async fn list_resource_templates(
941        &self,
942        _request: Option<PaginatedRequestParams>,
943        _context: RequestContext<RoleServer>,
944    ) -> Result<ListResourceTemplatesResult, McpError> {
945        Ok(ListResourceTemplatesResult {
946            resource_templates: vec![],
947            ..Default::default()
948        })
949    }
950
951    async fn read_resource(
952        &self,
953        request: ReadResourceRequestParams,
954        _context: RequestContext<RoleServer>,
955    ) -> Result<ReadResourceResponse, McpError> {
956        let uri = &request.uri;
957        let content = match uri.as_str() {
958            "rpfm://games" => serde_json::json!({
959                "supported_games": [
960                    {"key": "pharaoh_dynasties", "display_name": "Total War: Pharaoh Dynasties"},
961                    {"key": "pharaoh", "display_name": "Total War: Pharaoh"},
962                    {"key": "warhammer_3", "display_name": "Total War: Warhammer III"},
963                    {"key": "troy", "display_name": "A Total War Saga: Troy"},
964                    {"key": "three_kingdoms", "display_name": "Total War: Three Kingdoms"},
965                    {"key": "warhammer_2", "display_name": "Total War: Warhammer II"},
966                    {"key": "warhammer", "display_name": "Total War: Warhammer"},
967                    {"key": "thrones_of_britannia", "display_name": "A Total War Saga: Thrones of Britannia"},
968                    {"key": "attila", "display_name": "Total War: Attila"},
969                    {"key": "rome_2", "display_name": "Total War: Rome II"},
970                    {"key": "shogun_2", "display_name": "Total War: Shogun 2"},
971                    {"key": "napoleon", "display_name": "Total War: Napoleon"},
972                    {"key": "empire", "display_name": "Total War: Empire"},
973                    {"key": "arena", "display_name": "Total War: Arena"}
974                ]
975            }).to_string(),
976
977            "rpfm://enums/PFHFileType" => serde_json::json!({
978                "enum": "PFHFileType",
979                "description": "The type/priority of a PackFile. Games load packs in type order (Boot first, Movie last).",
980                "variants": [
981                    {"name": "Boot", "value": 0, "description": "Core game boot files, loaded first."},
982                    {"name": "Release", "value": 1, "description": "Main game data files."},
983                    {"name": "Patch", "value": 2, "description": "Official patch and update files."},
984                    {"name": "Mod", "value": 3, "description": "User mod files. This is the default for mods."},
985                    {"name": "Movie", "value": 4, "description": "Cinematic and always-loaded files, loaded last."}
986                ],
987                "json_example": "\"Mod\""
988            }).to_string(),
989
990            "rpfm://enums/CompressionFormat" => serde_json::json!({
991                "enum": "CompressionFormat",
992                "description": "Compression algorithm for pack file data.",
993                "variants": [
994                    {"name": "None", "description": "No compression (default)."},
995                    {"name": "Lzma1", "description": "Legacy LZMA compression (all PFH5 games)."},
996                    {"name": "Lz4", "description": "LZ4 compression (Warhammer 3 v6.2+)."},
997                    {"name": "Zstd", "description": "Zstandard compression (Warhammer 3 v6.2+)."}
998                ],
999                "json_example": "\"None\""
1000            }).to_string(),
1001
1002            "rpfm://enums/DataSource" => serde_json::json!({
1003                "enum": "DataSource",
1004                "description": "Identifies where data comes from when working with files.",
1005                "variants": [
1006                    {"name": "PackFile", "description": "Data from the user's currently open pack (mod files)."},
1007                    {"name": "GameFiles", "description": "Data from vanilla game files."},
1008                    {"name": "ParentFiles", "description": "Data from parent/dependency pack files."},
1009                    {"name": "AssKitFiles", "description": "Data from the Assembly Kit (modding tools)."},
1010                    {"name": "ExternalFile", "description": "Data from an external file on disk."}
1011                ],
1012                "json_example": "\"PackFile\""
1013            }).to_string(),
1014
1015            "rpfm://enums/ContainerPath" => serde_json::json!({
1016                "enum": "ContainerPath",
1017                "description": "A path reference inside a PackFile, pointing to either a file or a folder.",
1018                "variants": [
1019                    {
1020                        "name": "File",
1021                        "description": "Path to a single file inside the pack.",
1022                        "json_example": {"File": "db/land_units_tables/my_table"}
1023                    },
1024                    {
1025                        "name": "Folder",
1026                        "description": "Path to a folder inside the pack. Use empty string for root.",
1027                        "json_example": {"Folder": "db/land_units_tables"}
1028                    }
1029                ],
1030                "usage_notes": "Most tools accept a JSON array of ContainerPath objects, e.g. [{\"File\": \"path1\"}, {\"Folder\": \"path2\"}]"
1031            }).to_string(),
1032
1033            "rpfm://enums/NewFile" => serde_json::json!({
1034                "enum": "NewFile",
1035                "description": "Specifies what type of file to create inside a pack.",
1036                "variants": [
1037                    {
1038                        "name": "DB",
1039                        "description": "Create a new DB table. Args: [file_name, table_name, version].",
1040                        "json_example": {"DB": ["my_mod", "land_units_tables", 0]}
1041                    },
1042                    {
1043                        "name": "Loc",
1044                        "description": "Create a new localisation file. Arg: file_name.",
1045                        "json_example": {"Loc": "my_mod"}
1046                    },
1047                    {
1048                        "name": "Text",
1049                        "description": "Create a new text file. Args: [file_name, format]. Formats: Bat, Cpp, Html, Hlsl, Json, Js, Css, Lua, Markdown, Plain, Python, Sql, Xml, Yaml.",
1050                        "json_example": {"Text": ["my_script", "Lua"]}
1051                    },
1052                    {
1053                        "name": "AnimPack",
1054                        "description": "Create a new AnimPack file. Arg: file_name.",
1055                        "json_example": {"AnimPack": "my_anim"}
1056                    },
1057                    {
1058                        "name": "PortraitSettings",
1059                        "description": "Create a new portrait settings file. Args: [file_name, version, entries].",
1060                        "json_example": {"PortraitSettings": ["my_portraits", 3, []]}
1061                    },
1062                    {
1063                        "name": "VMD",
1064                        "description": "Create a new VMD file. Arg: file_name.",
1065                        "json_example": {"VMD": "my_vmd"}
1066                    },
1067                    {
1068                        "name": "WSModel",
1069                        "description": "Create a new WSModel file. Arg: file_name.",
1070                        "json_example": {"WSModel": "my_model"}
1071                    }
1072                ]
1073            }).to_string(),
1074
1075            "rpfm://enums/SupportedFormats" => serde_json::json!({
1076                "enum": "SupportedFormats",
1077                "description": "Video format options for CA VP8 video files.",
1078                "variants": [
1079                    {"name": "CaVp8", "description": "CA's custom VP8 format (default)."},
1080                    {"name": "Ivf", "description": "Standard VP8 IVF format."}
1081                ],
1082                "json_example": "\"CaVp8\""
1083            }).to_string(),
1084
1085            "rpfm://examples/global_search" => serde_json::json!({
1086                "description": "Example GlobalSearch JSON for use with global_search, global_search_replace_all, etc.",
1087                "example": {
1088                    "pattern": "old_unit_name",
1089                    "replace_text": "new_unit_name",
1090                    "case_sensitive": false,
1091                    "use_regex": false,
1092                    "sources": [{"Pack": "my_mod.pack"}],
1093                    "search_on": {
1094                        "anim": false, "anim_fragment_battle": false, "anim_pack": false,
1095                        "anims_table": false, "atlas": false, "audio": false, "bmd": false,
1096                        "db": true, "esf": false, "group_formations": false, "image": false,
1097                        "loc": true, "matched_combat": false, "pack": false,
1098                        "portrait_settings": false, "rigid_model": false, "sound_bank": false,
1099                        "text": true, "uic": false, "unit_variant": false, "unknown": false,
1100                        "video": false, "schema": false
1101                    },
1102                    "matches": {
1103                        "anim": [], "anim_fragment_battle": [], "anim_pack": [],
1104                        "anims_table": [], "atlas": [], "audio": [], "bmd": [],
1105                        "db": [], "esf": [], "group_formations": [], "image": [],
1106                        "loc": [], "matched_combat": [], "pack": [],
1107                        "portrait_settings": [], "rigid_model": [], "sound_bank": [],
1108                        "text": [], "uic": [], "unit_variant": [], "unknown": [],
1109                        "video": [], "schema": {"matches": []}
1110                    },
1111                    "game_key": "warhammer_3"
1112                },
1113                "notes": "The `matches` field is populated by the search results. When calling `global_search`, pass it empty. The `sources` field uses SearchSource: {\"Pack\": \"key\"}, \"ParentFiles\", \"GameFiles\", \"AssKitFiles\"."
1114            }).to_string(),
1115
1116            "rpfm://examples/optimizer_options" => serde_json::json!({
1117                "description": "OptimizerOptions struct with all boolean fields for pack optimization.",
1118                "example": {
1119                    "pack_remove_itm_files": true,
1120                    "pack_apply_compression": true,
1121                    "pack_apply_encryption": false,
1122                    "pack_remove_duplicated_files": false,
1123                    "db_import_datacores_into_twad_key_deletes": false,
1124                    "db_optimize_datacored_tables": false,
1125                    "table_remove_duplicated_entries": true,
1126                    "table_remove_itm_entries": true,
1127                    "table_remove_itnr_entries": true,
1128                    "table_remove_empty_file": true,
1129                    "text_remove_unused_xml_map_folders": false,
1130                    "text_remove_unused_xml_prefab_folder": false,
1131                    "text_remove_agf_files": false,
1132                    "text_remove_model_statistics_files": false,
1133                    "pts_remove_unused_art_sets": false,
1134                    "pts_remove_unused_variants": false,
1135                    "pts_remove_empty_masks": false,
1136                    "pts_remove_empty_file": false
1137                },
1138                "field_descriptions": {
1139                    "pack_remove_itm_files": "Remove files identical to vanilla (Identical To Master).",
1140                    "pack_apply_compression": "Apply the most modern compression format the active game supports (overriding the pack's configured one), so the next save compresses the files.",
1141                    "pack_apply_encryption": "Enable both index and data encryption, so the next save encrypts the pack. No-op on packs older than PFH4.",
1142                    "pack_remove_duplicated_files": "Remove case-insensitively duplicated files (same name ignoring casing) when their contents are identical, keeping the all-lowercase one or, failing that, the last one.",
1143                    "db_import_datacores_into_twad_key_deletes": "Import datacored tables into TWAD key deletes.",
1144                    "db_optimize_datacored_tables": "Optimize datacored tables.",
1145                    "table_remove_duplicated_entries": "Remove duplicate rows in tables.",
1146                    "table_remove_itm_entries": "Remove rows identical to vanilla.",
1147                    "table_remove_itnr_entries": "Remove rows identical to vanilla that are not referenced.",
1148                    "table_remove_empty_file": "Remove tables with no rows.",
1149                    "text_remove_unused_xml_map_folders": "Remove unused XML files in map folders.",
1150                    "text_remove_unused_xml_prefab_folder": "Remove unused XML files in prefab folders.",
1151                    "text_remove_agf_files": "Remove AGF files.",
1152                    "text_remove_model_statistics_files": "Remove model statistics files.",
1153                    "pts_remove_unused_art_sets": "Remove unused art sets in portrait settings.",
1154                    "pts_remove_unused_variants": "Remove unused variants in portrait settings.",
1155                    "pts_remove_empty_masks": "Remove empty masks in portrait settings.",
1156                    "pts_remove_empty_file": "Remove empty portrait settings files."
1157                }
1158            }).to_string(),
1159
1160            "rpfm://reference/initialization" => "\
1161RPFM MCP Server Initialization Guide
1162=====================================
1163
1164Before you can work with PackFiles, you must initialize the server session:
1165
1166Step 1: Set the game
1167    Call: set_game_selected(game_name: \"warhammer_3\", rebuild_dependencies: true)
1168    This loads the correct schemas and vanilla game data for the selected title.
1169    Valid game keys: pharaoh_dynasties, pharaoh, warhammer_3, troy, three_kingdoms,
1170    warhammer_2, warhammer, thrones_of_britannia, attila, rome_2, shogun_2,
1171    napoleon, empire, arena.
1172
1173Step 2: Verify schema is loaded
1174    Call: is_schema_loaded()
1175    If it returns false, call update_schemas() to download the latest schemas.
1176
1177Step 3: Open a PackFile
1178    Call: open_packfiles(paths: [\"/path/to/my_mod.pack\"])
1179    The response returns pack info including the pack_key you'll use for all
1180    subsequent operations.
1181
1182Step 4: Verify dependencies (optional but recommended)
1183    Call: is_there_a_dependency_database(value: true)
1184    If false, call generate_dependencies_cache() to build the dependency database.
1185
1186After initialization, use list_open_packs() to see all open pack keys at any time.
1187".to_string(),
1188
1189            "rpfm://reference/path_conventions" => "\
1190Total War PackFile Path Conventions
1191====================================
1192
1193Files inside PackFiles follow specific path conventions:
1194
1195DB Tables:
1196    db/<table_name>/<file_name>
1197    Example: db/land_units_tables/my_mod
1198    Example: db/unit_stats_land_tables/custom_units
1199
1200Localisation (Loc) files:
1201    text/db/<file_name>.loc
1202    text/<file_name>.loc
1203    Example: text/db/my_mod.loc
1204
1205Scripts:
1206    script/<path>.lua
1207    script/campaign/mod/<script_name>.lua
1208    Example: script/campaign/mod/my_mod_script.lua
1209    After editing a script, run `diagnostics_check`: it reports Lua syntax errors, invalid DB keys, unknown methods,
1210    wrong argument counts and unknown events (all but syntax errors need the game's Assembly Kit installed).
1211    To check what a script does, write tests for it and run them with `lua_run_tests`.
1212
1213UI Images:
1214    ui/<path>.png
1215    Path may vary depending on the purpose of the image.
1216
1217Models and Animations:
1218    variantmeshes/<path>
1219    animations/<path>
1220    Example: variantmeshes/wh_variantmodels/hu1/my_unit/my_unit.wsmodel
1221
1222Audio:
1223    audio/<path>.bnk
1224
1225Maps:
1226    terrain/tiles/battle/<map_name>/
1227".to_string(),
1228
1229            _ => {
1230                return Err(McpError {
1231                    code: ErrorCode::INVALID_PARAMS,
1232                    message: format!("Unknown resource URI: {uri}").into(),
1233                    data: None,
1234                });
1235            }
1236        };
1237
1238        Ok(ReadResourceResult::new(vec![ResourceContents::text(content, uri.clone())]).into())
1239    }
1240
1241    //-----------------------------------------------------------------------//
1242    // Completions
1243    //-----------------------------------------------------------------------//
1244
1245    async fn complete(
1246        &self,
1247        request: CompleteRequestParams,
1248        _context: RequestContext<RoleServer>,
1249    ) -> Result<CompleteResult, McpError> {
1250        let argument_name = &request.argument.name;
1251        let partial = &request.argument.value;
1252
1253        let candidates: Vec<String> = match argument_name.as_str() {
1254            "game_name" | "game_key" | "game" => {
1255                let games = vec![
1256                    "pharaoh_dynasties", "pharaoh", "warhammer_3", "troy",
1257                    "three_kingdoms", "warhammer_2", "warhammer",
1258                    "thrones_of_britannia", "attila", "rome_2", "shogun_2",
1259                    "napoleon", "empire", "arena",
1260                ];
1261                games.into_iter()
1262                    .filter(|g| g.starts_with(partial))
1263                    .map(String::from)
1264                    .collect()
1265            },
1266            "pack_file_type" => {
1267                let types = vec!["\"Boot\"", "\"Release\"", "\"Patch\"", "\"Mod\"", "\"Movie\""];
1268                types.into_iter()
1269                    .filter(|t| t.starts_with(partial))
1270                    .map(String::from)
1271                    .collect()
1272            },
1273            "format" => {
1274                // Could be CompressionFormat or SupportedFormats depending on tool
1275                let formats = vec![
1276                    "\"None\"", "\"Lzma1\"", "\"Lz4\"", "\"Zstd\"",
1277                    "\"CaVp8\"", "\"Ivf\"",
1278                ];
1279                formats.into_iter()
1280                    .filter(|f| f.starts_with(partial))
1281                    .map(String::from)
1282                    .collect()
1283            },
1284            "source" => {
1285                let sources = vec![
1286                    "\"PackFile\"", "\"GameFiles\"", "\"ParentFiles\"",
1287                    "\"AssKitFiles\"", "\"ExternalFile\"",
1288                ];
1289                sources.into_iter()
1290                    .filter(|s| s.starts_with(partial))
1291                    .map(String::from)
1292                    .collect()
1293            },
1294            _ => vec![],
1295        };
1296
1297        let total = candidates.len() as u32;
1298        let values: Vec<String> = candidates.into_iter().take(100).collect();
1299        let has_more = total > 100;
1300
1301        Ok(CompleteResult::new(CompletionInfo::with_pagination(
1302            values,
1303            Some(total),
1304            has_more,
1305        ).map_err(|e| McpError {
1306            code: ErrorCode::INTERNAL_ERROR,
1307            message: format!("Failed to build completion info: {e}").into(),
1308            data: None,
1309        })?))
1310    }
1311
1312}
1313
1314#[tool_router]
1315impl McpServer {
1316
1317    pub fn new(session: Arc<Session>) -> Self {
1318        Self {
1319            session,
1320            tool_router: McpServer::tool_router(),
1321            prompt_router: McpServer::prompt_router(),
1322        }
1323    }
1324
1325    //-----------------------------------------------------------------------//
1326    // Existing tools
1327    //-----------------------------------------------------------------------//
1328
1329    #[tool(name = "call_command", description = "Call any IPC command directly. Use this for commands not yet wrapped as named tools.")]
1330    pub async fn call_command(&self, params: Parameters<CallCommandArgs>) -> Result<CallToolResult, McpError> {
1331        let command: Command = parse_json!(&params.0.command);
1332        send_and_respond!(self, "call_command", command)
1333    }
1334
1335    //-----------------------------------------------------------------------//
1336    // Pack Lifecycle
1337    //-----------------------------------------------------------------------//
1338
1339    #[tool(description = "Create a new empty PackFile.")]
1340    pub async fn new_pack(&self) -> Result<CallToolResult, McpError> {
1341        send_and_respond!(self, "new_pack", Command::NewPack)
1342    }
1343
1344    #[tool(description = "Open one or more PackFiles. Returns the info about the open pack.")]
1345    pub async fn open_packfiles(&self, params: Parameters<OpenPackfilesArgs>) -> Result<CallToolResult, McpError> {
1346        send_and_respond!(self, "open_packfiles", Command::OpenPackFiles(params.0.paths))
1347    }
1348
1349    #[tool(description = "Save the pack identified by `pack_key`.")]
1350    pub async fn save_packfile(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1351        send_and_respond!(self, "save_packfile", Command::SavePack(params.0.pack_key))
1352    }
1353
1354    #[tool(description = "Close the pack identified by `pack_key` without saving. Any unsaved changes will be lost.")]
1355    pub async fn close_pack(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1356        send_and_respond!(self, "close_pack", Command::ClosePack(params.0.pack_key))
1357    }
1358
1359    #[tool(description = "Save the pack identified by `pack_key` to a new path.")]
1360    pub async fn save_pack_as(&self, params: Parameters<PackKeyPathArg>) -> Result<CallToolResult, McpError> {
1361        send_and_respond!(self, "save_pack_as", Command::SavePackAs(params.0.pack_key, params.0.path))
1362    }
1363
1364    #[tool(description = "Clean the pack identified by `pack_key` from corrupted files and save to a path. Use if normal save fails.")]
1365    pub async fn clean_and_save_pack_as(&self, params: Parameters<PackKeyPathArg>) -> Result<CallToolResult, McpError> {
1366        send_and_respond!(self, "clean_and_save_pack_as", Command::CleanAndSavePackAs(params.0.pack_key, params.0.path))
1367    }
1368
1369    #[tool(description = "Trigger a backup autosave for the pack identified by `pack_key`.")]
1370    pub async fn trigger_backup_autosave(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1371        send_and_respond!(self, "trigger_backup_autosave", Command::TriggerBackupAutosave(params.0.pack_key))
1372    }
1373
1374    #[tool(description = "Open all CA (vanilla) PackFiles for the selected game as one merged PackFile.")]
1375    pub async fn load_all_ca_pack_files(&self) -> Result<CallToolResult, McpError> {
1376        send_and_respond!(self, "load_all_ca_pack_files", Command::LoadAllCAPackFiles)
1377    }
1378
1379    //-----------------------------------------------------------------------//
1380    // Pack Metadata
1381    //-----------------------------------------------------------------------//
1382
1383    #[tool(description = "Set the type of the pack identified by `pack_key`. Valid PFHFileType values: \"Boot\", \"Release\", \"Patch\", \"Mod\", \"Movie\". Example: pack_file_type = \"\\\"Mod\\\"\"")]
1384    pub async fn set_pack_file_type(&self, params: Parameters<SetPackFileTypeArgs>) -> Result<CallToolResult, McpError> {
1385        let pfh_type = parse_json!(&params.0.pack_file_type);
1386        send_and_respond!(self, "set_pack_file_type", Command::SetPackFileType(params.0.pack_key, pfh_type))
1387    }
1388
1389    #[tool(description = "Change the compression format of the pack identified by `pack_key`. Valid formats: \"None\", \"Lzma1\" (legacy), \"Lz4\" (WH3 6.2+), \"Zstd\" (WH3 6.2+). Example: format = \"\\\"None\\\"\"")]
1390    pub async fn change_compression_format(&self, params: Parameters<ChangeCompressionFormatArgs>) -> Result<CallToolResult, McpError> {
1391        let format = parse_json!(&params.0.format);
1392        send_and_respond!(self, "change_compression_format", Command::ChangeCompressionFormat(params.0.pack_key, format))
1393    }
1394
1395    #[tool(description = "Change whether the pack index includes timestamps for the pack identified by `pack_key`.")]
1396    pub async fn change_index_includes_timestamp(&self, params: Parameters<PackKeyBoolArg>) -> Result<CallToolResult, McpError> {
1397        send_and_respond!(self, "change_index_includes_timestamp", Command::ChangeIndexIncludesTimestamp(params.0.pack_key, params.0.value))
1398    }
1399
1400    #[tool(description = "Change whether the pack index (file paths, sizes and timestamps) is encrypted for the pack identified by `pack_key`. Only PFH4 and newer packs support enabling it.")]
1401    pub async fn change_index_is_encrypted(&self, params: Parameters<PackKeyBoolArg>) -> Result<CallToolResult, McpError> {
1402        send_and_respond!(self, "change_index_is_encrypted", Command::ChangeIndexIsEncrypted(params.0.pack_key, params.0.value))
1403    }
1404
1405    #[tool(description = "Change whether the file data is encrypted for the pack identified by `pack_key`. Only PFH4 and newer packs support enabling it.")]
1406    pub async fn change_data_is_encrypted(&self, params: Parameters<PackKeyBoolArg>) -> Result<CallToolResult, McpError> {
1407        send_and_respond!(self, "change_data_is_encrypted", Command::ChangeDataIsEncrypted(params.0.pack_key, params.0.value))
1408    }
1409
1410    #[tool(description = "Get the file path of the pack identified by `pack_key`.")]
1411    pub async fn get_pack_file_path(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1412        send_and_respond!(self, "get_pack_file_path", Command::GetPackFilePath(params.0.pack_key))
1413    }
1414
1415    #[tool(description = "Get the file name of the pack identified by `pack_key`.")]
1416    pub async fn get_pack_file_name(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1417        send_and_respond!(self, "get_pack_file_name", Command::GetPackFileName(params.0.pack_key))
1418    }
1419
1420    #[tool(description = "Get the settings of the pack identified by `pack_key`.")]
1421    pub async fn get_pack_settings(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1422        send_and_respond!(self, "get_pack_settings", Command::GetPackSettings(params.0.pack_key))
1423    }
1424
1425    #[tool(description = "Set the settings of the pack identified by `pack_key`. The `settings` is a PackSettings JSON object containing pack-level configuration.")]
1426    pub async fn set_pack_settings(&self, params: Parameters<SetPackSettingsArgs>) -> Result<CallToolResult, McpError> {
1427        let settings = parse_json!(&params.0.settings);
1428        send_and_respond!(self, "set_pack_settings", Command::SetPackSettings(params.0.pack_key, settings))
1429    }
1430
1431    #[tool(description = "Get the list of PackFiles marked as dependencies of the pack identified by `pack_key`.")]
1432    pub async fn get_dependency_pack_files_list(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1433        send_and_respond!(self, "get_dependency_pack_files_list", Command::GetDependencyPackFilesList(params.0.pack_key))
1434    }
1435
1436    #[tool(description = "Set the list of PackFiles marked as dependencies for the pack identified by `pack_key`. The `list` is a JSON array of [enabled, pack_name] pairs, e.g. [[true, \"other_mod.pack\"], [false, \"disabled_mod.pack\"]].")]
1437    pub async fn set_dependency_pack_files_list(&self, params: Parameters<SetDependencyPackFilesListArgs>) -> Result<CallToolResult, McpError> {
1438        let list = parse_json!(&params.0.list);
1439        send_and_respond!(self, "set_dependency_pack_files_list", Command::SetDependencyPackFilesList(params.0.pack_key, list))
1440    }
1441
1442    //-----------------------------------------------------------------------//
1443    // File Operations
1444    //-----------------------------------------------------------------------//
1445
1446    #[tool(description = "Decode a file from the pack identified by `pack_key`. The `path` is the internal file path (e.g. \"db/land_units_tables/my_mod\"). The `source` is the data source: \"PackFile\" (user mod), \"GameFiles\" (vanilla), \"ParentFiles\" (dependency mods), \"AssKitFiles\", or \"ExternalFile\". Returns the decoded file content as JSON (RFileDecoded).")]
1447    pub async fn decode_packed_file(&self, params: Parameters<DecodePackedFileArgs>) -> Result<CallToolResult, McpError> {
1448        send_and_respond!(self, "decode_packed_file", Command::DecodePackedFile(params.0.pack_key, params.0.path, params.0.source))
1449    }
1450
1451    #[tool(description = "Create a new file inside the pack identified by `pack_key`. The `path` is the destination path (e.g. \"db/land_units_tables/my_mod\"). NewFile types: {\"DB\": [\"file_name\", \"table_name\", version]}, {\"Loc\": \"name\"}, {\"Text\": [\"name\", \"Plain\"]}, {\"AnimPack\": \"name\"}, {\"VMD\": \"name\"}, {\"WSModel\": \"name\"}, {\"PortraitSettings\": [\"name\", version, []]}.")]
1452    pub async fn new_packed_file(&self, params: Parameters<NewPackedFileArgs>) -> Result<CallToolResult, McpError> {
1453        let new_file = parse_json!(&params.0.new_file);
1454        send_and_respond!(self, "new_packed_file", Command::NewPackedFile(params.0.pack_key, params.0.path, new_file))
1455    }
1456
1457    #[tool(description = "Add files from disk to the pack identified by `pack_key`. The `source_paths` are filesystem paths. The `destination_paths` is a JSON array of ContainerPath: [{\"File\": \"db/table/file\"}, {\"Folder\": \"ui/images\"}]. Optionally set `ignore_paths` to skip certain files.")]
1458    pub async fn add_packed_files(&self, params: Parameters<AddPackedFilesArgs>) -> Result<CallToolResult, McpError> {
1459        let dest: Vec<ContainerPath> = parse_json!(&params.0.destination_paths);
1460        send_and_respond!(self, "add_packed_files", Command::AddPackedFiles(params.0.pack_key, params.0.source_paths, dest, params.0.ignore_paths))
1461    }
1462
1463    #[tool(description = "Add files from another PackFile to the pack identified by `pack_key`. The `source_pack_path` is the pack path. The `container_paths` is a JSON array of ContainerPath: [{\"File\": \"path\"}].")]
1464    pub async fn add_packed_files_from_pack_file(&self, params: Parameters<AddPackedFilesFromPackFileArgs>) -> Result<CallToolResult, McpError> {
1465        let paths: Vec<ContainerPath> = parse_json!(&params.0.container_paths);
1466        send_and_respond!(self, "add_packed_files_from_pack_file", Command::AddPackedFilesFromPackFile(params.0.pack_key, params.0.source_pack_path, paths))
1467    }
1468
1469    #[tool(description = "Copy files from the pack identified by `source_pack_key` into an AnimPack owned by `pack_key` (the two may differ). The `container_paths` is a JSON array of ContainerPath, e.g. [{\"File\": \"animations/anim.anim\"}]. The `animpack_path` is the AnimPack's internal path.")]
1470    pub async fn add_packed_files_from_pack_file_to_animpack(&self, params: Parameters<AddPackedFilesFromPackFileToAnimpackArgs>) -> Result<CallToolResult, McpError> {
1471        let paths: Vec<ContainerPath> = parse_json!(&params.0.container_paths);
1472        send_and_respond!(self, "add_packed_files_from_pack_file_to_animpack", Command::AddPackedFilesFromPackFileToAnimpack(params.0.source_pack_key, params.0.pack_key, params.0.animpack_path, paths))
1473    }
1474
1475    #[tool(description = "Copy files from an AnimPack owned by `anim_pack_key` into the destination pack `pack_key` (the two may differ). The `source` is the DataSource (\"PackFile\", \"GameFiles\", etc.); `anim_pack_key` is only used when it is \"PackFile\". The `animpack_path` is the AnimPack's internal path. The `container_paths` is a JSON array of ContainerPath, e.g. [{\"File\": \"animations/anim.anim\"}].")]
1476    pub async fn add_packed_files_from_animpack(&self, params: Parameters<AddPackedFilesFromAnimpackArgs>) -> Result<CallToolResult, McpError> {
1477        let paths: Vec<ContainerPath> = parse_json!(&params.0.container_paths);
1478        send_and_respond!(self, "add_packed_files_from_animpack", Command::AddPackedFilesFromAnimpack(params.0.anim_pack_key, params.0.pack_key, params.0.source, params.0.animpack_path, paths))
1479    }
1480
1481    #[tool(description = "Delete files from the pack identified by `pack_key`. The `paths` is a JSON array of ContainerPath: [{\"File\": \"path/to/file\"}, {\"Folder\": \"path/to/folder\"}].")]
1482    pub async fn delete_packed_files(&self, params: Parameters<ContainerPathsArg>) -> Result<CallToolResult, McpError> {
1483        let paths: Vec<ContainerPath> = parse_json!(&params.0.paths);
1484        send_and_respond!(self, "delete_packed_files", Command::DeletePackedFiles(params.0.pack_key, paths))
1485    }
1486
1487    #[tool(description = "Delete files from an AnimPack in the pack identified by `pack_key`. The `animpack_path` is the AnimPack's internal path. The `container_paths` is a JSON array of ContainerPath, e.g. [{\"File\": \"animations/anim.anim\"}].")]
1488    pub async fn delete_from_animpack(&self, params: Parameters<DeleteFromAnimpackArgs>) -> Result<CallToolResult, McpError> {
1489        let paths: Vec<ContainerPath> = parse_json!(&params.0.container_paths);
1490        send_and_respond!(self, "delete_from_animpack", Command::DeleteFromAnimpack(params.0.pack_key, params.0.animpack_path, paths))
1491    }
1492
1493    #[tool(description = "Extract files from the pack identified by `pack_key` to disk. The `source_paths` is a JSON object mapping DataSource to ContainerPath arrays, e.g. {\"PackFile\": [{\"File\": \"db/table/file\"}]}. Set `export_as_tsv: true` to export tables as TSV files.")]
1494    pub async fn extract_packed_files(&self, params: Parameters<ExtractPackedFilesArgs>) -> Result<CallToolResult, McpError> {
1495        let source: BTreeMap<DataSource, Vec<ContainerPath>> = parse_json!(&params.0.source_paths);
1496        send_and_respond!(self, "extract_packed_files", Command::ExtractPackedFiles(params.0.pack_key, source, params.0.destination_path, params.0.export_as_tsv))
1497    }
1498
1499    #[tool(description = "Rename files in the pack identified by `pack_key`. The `renames` is a JSON array of [old, new] ContainerPath pairs, e.g. [[{\"File\": \"old/path\"}, {\"File\": \"new/path\"}]].")]
1500    pub async fn rename_packed_files(&self, params: Parameters<RenamePackedFilesArgs>) -> Result<CallToolResult, McpError> {
1501        let renames: Vec<(ContainerPath, ContainerPath)> = parse_json!(&params.0.renames);
1502        send_and_respond!(self, "rename_packed_files", Command::RenamePackedFiles(params.0.pack_key, renames))
1503    }
1504
1505    #[tool(description = "Copy files to the internal clipboard. The `paths_by_pack` is a JSON object mapping pack key to ContainerPath arrays, e.g. {\"my_pack.pack\": [{\"File\": \"db/table/file\"}]}. Use `paste_packed_files` to paste afterwards.")]
1506    pub async fn copy_packed_files(&self, params: Parameters<CopyOrCutPackedFilesArgs>) -> Result<CallToolResult, McpError> {
1507        let paths_by_pack: BTreeMap<String, Vec<ContainerPath>> = parse_json!(&params.0.paths_by_pack);
1508        send_and_respond!(self, "copy_packed_files", Command::CopyPackedFiles(paths_by_pack))
1509    }
1510
1511    #[tool(description = "Cut files to the internal clipboard. Same as copy, but files will be removed from the source pack on paste. The `paths_by_pack` is a JSON object mapping pack key to ContainerPath arrays. Use `paste_packed_files` to paste afterwards.")]
1512    pub async fn cut_packed_files(&self, params: Parameters<CopyOrCutPackedFilesArgs>) -> Result<CallToolResult, McpError> {
1513        let paths_by_pack: BTreeMap<String, Vec<ContainerPath>> = parse_json!(&params.0.paths_by_pack);
1514        send_and_respond!(self, "cut_packed_files", Command::CutPackedFiles(paths_by_pack))
1515    }
1516
1517    #[tool(description = "Paste files from the internal clipboard into the pack identified by `pack_key`. The `destination_path` is the folder path to paste into (empty string for root). Returns the added paths, any cut-deleted paths, and the source pack key.")]
1518    pub async fn paste_packed_files(&self, params: Parameters<PastePackedFilesArgs>) -> Result<CallToolResult, McpError> {
1519        send_and_respond!(self, "paste_packed_files", Command::PastePackedFiles(params.0.pack_key, params.0.destination_path))
1520    }
1521
1522    #[tool(description = "Duplicate files in-place within the same pack. Files are cloned with a numeric suffix to avoid name collisions. The `paths` is a JSON array of ContainerPath, e.g. [{\"File\": \"db/table/file\"}].")]
1523    pub async fn duplicate_packed_files(&self, params: Parameters<DuplicatePackedFilesArgs>) -> Result<CallToolResult, McpError> {
1524        let paths: Vec<ContainerPath> = parse_json!(&params.0.paths);
1525        send_and_respond!(self, "duplicate_packed_files", Command::DuplicatePackedFiles(params.0.pack_key, paths))
1526    }
1527
1528    #[tool(description = "Save an edited decoded file back to the pack identified by `pack_key`. The `path` is the internal path (e.g. \"db/land_units_tables/my_mod\"). The `data` is the modified RFileDecoded JSON (same structure returned by `decode_packed_file`).")]
1529    pub async fn save_packed_file_from_view(&self, params: Parameters<SavePackedFileFromViewArgs>) -> Result<CallToolResult, McpError> {
1530        let data: RFileDecoded = parse_json!(&params.0.data);
1531        send_and_respond!(self, "save_packed_file_from_view", Command::SavePackedFileFromView(params.0.pack_key, params.0.path, data))
1532    }
1533
1534    #[tool(description = "Save a file from an external program back to the pack identified by `pack_key`.")]
1535    pub async fn save_packed_file_from_external_view(&self, params: Parameters<SavePackedFileFromExternalViewArgs>) -> Result<CallToolResult, McpError> {
1536        send_and_respond!(self, "save_packed_file_from_external_view", Command::SavePackedFileFromExternalView(params.0.pack_key, params.0.internal_path, params.0.external_path))
1537    }
1538
1539    #[tool(description = "Save files to the pack identified by `pack_key` and optionally optimize afterward. The `files` is a JSON array of RFile objects (as returned by decode/get operations). Set `optimize` to true to remove unchanged data after saving.")]
1540    pub async fn save_packed_files_to_pack_file_and_clean(&self, params: Parameters<SavePackedFilesToPackFileAndCleanArgs>) -> Result<CallToolResult, McpError> {
1541        let files: Vec<RFile> = parse_json!(&params.0.files);
1542        send_and_respond!(self, "save_packed_files_to_pack_file_and_clean", Command::SavePackedFilesToPackFileAndClean(params.0.pack_key, files, params.0.optimize))
1543    }
1544
1545    #[tool(description = "Get the raw binary data of a file in the pack identified by `pack_key`.")]
1546    pub async fn get_packed_file_raw_data(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1547        send_and_respond!(self, "get_packed_file_raw_data", Command::GetPackedFileRawData(params.0.pack_key, params.0.value))
1548    }
1549
1550    #[tool(description = "Open a file in the system's default program from the pack identified by `pack_key`. The `source` is the DataSource (\"PackFile\", \"GameFiles\", etc.). The `container_path` is a ContainerPath JSON, e.g. {\"File\": \"db/table/file\"}.")]
1551    pub async fn open_packed_file_in_external_program(&self, params: Parameters<OpenPackedFileInExternalProgramArgs>) -> Result<CallToolResult, McpError> {
1552        let cp: ContainerPath = parse_json!(&params.0.container_path);
1553        send_and_respond!(self, "open_packed_file_in_external_program", Command::OpenPackedFileInExternalProgram(params.0.pack_key, params.0.source, cp))
1554    }
1555
1556    #[tool(description = "Open the folder containing the pack identified by `pack_key` in the file manager.")]
1557    pub async fn open_containing_folder(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1558        send_and_respond!(self, "open_containing_folder", Command::OpenContainingFolder(params.0.pack_key))
1559    }
1560
1561    #[tool(description = "Clean the decode cache for the provided paths in the pack identified by `pack_key`. The `paths` is a JSON array of ContainerPath, e.g. [{\"File\": \"db/land_units_tables/my_mod\"}, {\"Folder\": \"db\"}].")]
1562    pub async fn clean_cache(&self, params: Parameters<ContainerPathsArg>) -> Result<CallToolResult, McpError> {
1563        let paths: Vec<ContainerPath> = parse_json!(&params.0.paths);
1564        send_and_respond!(self, "clean_cache", Command::CleanCache(params.0.pack_key, paths))
1565    }
1566
1567    #[tool(description = "Check if a folder exists in the pack identified by `pack_key`.")]
1568    pub async fn folder_exists(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1569        send_and_respond!(self, "folder_exists", Command::FolderExists(params.0.pack_key, params.0.value))
1570    }
1571
1572    #[tool(description = "Check if a file exists in the pack identified by `pack_key`.")]
1573    pub async fn packed_file_exists(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1574        send_and_respond!(self, "packed_file_exists", Command::PackedFileExists(params.0.pack_key, params.0.value))
1575    }
1576
1577    #[tool(description = "Get the info of one or more files in the pack identified by `pack_key`.")]
1578    pub async fn get_packed_files_info(&self, params: Parameters<PackKeyStringsArg>) -> Result<CallToolResult, McpError> {
1579        send_and_respond!(self, "get_packed_files_info", Command::GetPackedFilesInfo(params.0.pack_key, params.0.values))
1580    }
1581
1582    #[tool(description = "Get the info of a single file in the pack identified by `pack_key`.")]
1583    pub async fn get_rfile_info(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1584        send_and_respond!(self, "get_rfile_info", Command::GetRFileInfo(params.0.pack_key, params.0.value))
1585    }
1586
1587    //-----------------------------------------------------------------------//
1588    // Game Selection
1589    //-----------------------------------------------------------------------//
1590
1591    #[tool(description = "Get the currently selected game key.")]
1592    pub async fn get_game_selected(&self) -> Result<CallToolResult, McpError> {
1593        send_and_respond!(self, "get_game_selected", Command::GetGameSelected)
1594    }
1595
1596    #[tool(description = "Set the current game. Valid game keys: pharaoh_dynasties, pharaoh, warhammer_3, troy, three_kingdoms, warhammer_2, warhammer, thrones_of_britannia, attila, rome_2, shogun_2, napoleon, empire, arena. Set rebuild_dependencies to true on first call to load schemas and vanilla data.")]
1597    pub async fn set_game_selected(&self, params: Parameters<SetGameSelectedArgs>) -> Result<CallToolResult, McpError> {
1598        send_and_respond!(self, "set_game_selected", Command::SetGameSelected(params.0.game_name, params.0.rebuild_dependencies))
1599    }
1600
1601    //-----------------------------------------------------------------------//
1602    // Dependencies
1603    //-----------------------------------------------------------------------//
1604
1605    #[tool(description = "Generate the dependencies cache for the selected game. This can take a long time (more than 30 seconds), depending on your CPU and disk read speed. If the client is not careful, it can take enough time that the client may trigger a timeout.")]
1606    pub async fn generate_dependencies_cache(&self) -> Result<CallToolResult, McpError> {
1607        send_and_respond!(self, "generate_dependencies_cache", Command::GenerateDependenciesCache)
1608    }
1609
1610    #[tool(description = "Rebuild dependencies. Pass true for full rebuild, false for mod-specific only.")]
1611    pub async fn rebuild_dependencies(&self, params: Parameters<BoolArg>) -> Result<CallToolResult, McpError> {
1612        send_and_respond!(self, "rebuild_dependencies", Command::RebuildDependencies(params.0.value))
1613    }
1614
1615    #[tool(description = "Check if there is a dependency database loaded. Pass true to ensure AssKit data is included.")]
1616    pub async fn is_there_a_dependency_database(&self, params: Parameters<BoolArg>) -> Result<CallToolResult, McpError> {
1617        send_and_respond!(self, "is_there_a_dependency_database", Command::IsThereADependencyDatabase(params.0.value))
1618    }
1619
1620    #[tool(description = "Get the table names of all DB files in dependency PackFiles.")]
1621    pub async fn get_table_list_from_dependency_pack_file(&self) -> Result<CallToolResult, McpError> {
1622        send_and_respond!(self, "get_table_list_from_dependency_pack_file", Command::GetTableListFromDependencyPackFile)
1623    }
1624
1625    #[tool(description = "Get custom table names (start_pos_, twad_ prefixes) from the schema.")]
1626    pub async fn get_custom_table_list(&self) -> Result<CallToolResult, McpError> {
1627        send_and_respond!(self, "get_custom_table_list", Command::GetCustomTableList)
1628    }
1629
1630    #[tool(description = "Get the version of a table from the dependency database.")]
1631    pub async fn get_table_version_from_dependency_pack_file(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
1632        send_and_respond!(self, "get_table_version_from_dependency_pack_file", Command::GetTableVersionFromDependencyPackFile(params.0.value))
1633    }
1634
1635    #[tool(description = "Get the definition of a table from the dependency database. NOTE: the returned `fields` list is the raw on-disk field layout, not what row data looks like (e.g. colour columns are split into separate r/g/b fields here). Pass the definition to `fields_processed` to get the field list/count that rows must actually match.")]
1636    pub async fn get_table_definition_from_dependency_pack_file(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
1637        send_and_respond!(self, "get_table_definition_from_dependency_pack_file", Command::GetTableDefinitionFromDependencyPackFile(params.0.value))
1638    }
1639
1640    #[tool(description = "Get table data from dependencies by table name. NOTE: each returned file's decoded `data` rows are shaped per the PROCESSED fields (colour groups merged, bitwise/enum expanded), but the `definition.fields` bundled in the same file is the RAW on-disk layout and will have a different length/order — do not zip row cells against `definition.fields`. Call `fields_processed` on that definition to get the field list that actually lines up with `data`.")]
1641    pub async fn get_tables_from_dependencies(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
1642        send_and_respond!(self, "get_tables_from_dependencies", Command::GetTablesFromDependencies(params.0.value))
1643    }
1644
1645    #[tool(description = "Import files from dependencies into the pack identified by `pack_key`. The `paths` is a JSON object mapping DataSource to ContainerPath arrays, e.g. {\"GameFiles\": [{\"File\": \"db/table/file\"}]}.")]
1646    pub async fn import_dependencies_to_open_pack_file(&self, params: Parameters<ImportDependenciesArgs>) -> Result<CallToolResult, McpError> {
1647        let paths: BTreeMap<DataSource, Vec<ContainerPath>> = parse_json!(&params.0.paths);
1648        send_and_respond!(self, "import_dependencies_to_open_pack_file", Command::ImportDependenciesToOpenPackFile(params.0.pack_key, paths))
1649    }
1650
1651    #[tool(description = "Get files from all known sources (PackFile, GameFiles, ParentFiles). The `paths` is a JSON array of ContainerPath, e.g. [{\"File\": \"db/land_units_tables/some_file\"}]. Set `lowercase` to true to normalize path casing.")]
1652    pub async fn get_rfiles_from_all_sources(&self, params: Parameters<GetRFilesFromAllSourcesArgs>) -> Result<CallToolResult, McpError> {
1653        let paths: Vec<ContainerPath> = parse_json!(&params.0.paths);
1654        send_and_respond!(self, "get_rfiles_from_all_sources", Command::GetRFilesFromAllSources(paths, params.0.lowercase))
1655    }
1656
1657    #[tool(description = "Get all file names under a path prefix across all data sources (PackFile, GameFiles, ParentFiles). The `path` is a ContainerPath JSON, e.g. {\"Folder\": \"db/land_units_tables\"} to list all files under that folder.")]
1658    pub async fn get_packed_files_names_starting_with_path_from_all_sources(&self, params: Parameters<ContainerPathArg>) -> Result<CallToolResult, McpError> {
1659        let path: ContainerPath = parse_json!(&params.0.path);
1660        send_and_respond!(self, "get_packed_files_names_starting_with_path_from_all_sources", Command::GetPackedFilesNamesStartingWitPathFromAllSources(path))
1661    }
1662
1663    #[tool(description = "Get local art set IDs from campaign_character_arts_tables in the pack identified by `pack_key`.")]
1664    pub async fn local_art_set_ids(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1665        send_and_respond!(self, "local_art_set_ids", Command::LocalArtSetIds(params.0.pack_key))
1666    }
1667
1668    #[tool(description = "Get art set IDs from dependencies' campaign_character_arts_tables.")]
1669    pub async fn dependencies_art_set_ids(&self) -> Result<CallToolResult, McpError> {
1670        send_and_respond!(self, "dependencies_art_set_ids", Command::DependenciesArtSetIds)
1671    }
1672
1673    #[tool(description = "Get the distinct values of the column `column_name` of the DB table `table_name` (like `factions_tables`), from the open packs, their parent packs and vanilla.")]
1674    pub async fn dependencies_column_values(&self, params: Parameters<TableColumnArgs>) -> Result<CallToolResult, McpError> {
1675        send_and_respond!(self, "dependencies_column_values", Command::DependenciesColumnValues(params.0.table_name, params.0.column_name))
1676    }
1677
1678    //-----------------------------------------------------------------------//
1679    // Search
1680    //-----------------------------------------------------------------------//
1681
1682    #[tool(description = "Run a global search across the pack identified by `pack_key`. The `search` is a GlobalSearch JSON with fields: pattern (string), replace_text (string), case_sensitive (bool), use_regex (bool), search_on ({db: bool, loc: bool, text: bool, ...}), sources ([{\"Pack\": \"key\"}]), game_key (string). See the `rpfm://examples/global_search` resource for a full example.")]
1683    pub async fn global_search(&self, params: Parameters<GlobalSearchArgs>) -> Result<CallToolResult, McpError> {
1684        let search = parse_json!(&params.0.search);
1685        send_and_respond!(self, "global_search", Command::GlobalSearch(params.0.pack_key, search))
1686    }
1687
1688    #[tool(description = "Replace specific matches in a global search for the pack identified by `pack_key`. The `search` is the same GlobalSearch JSON used in `global_search` (see `rpfm://examples/global_search` resource). The `matches` is a JSON array of MatchHolder objects from the search results — include only the matches you want to replace.")]
1689    pub async fn global_search_replace_matches(&self, params: Parameters<GlobalSearchReplaceMatchesArgs>) -> Result<CallToolResult, McpError> {
1690        let search = parse_json!(&params.0.search);
1691        let matches = parse_json!(&params.0.matches);
1692        send_and_respond!(self, "global_search_replace_matches", Command::GlobalSearchReplaceMatches(params.0.pack_key, search, matches))
1693    }
1694
1695    #[tool(description = "Replace all matches in a global search for the pack identified by `pack_key`. The `search` is a GlobalSearch JSON with the `replace_text` field set to the replacement string. See `rpfm://examples/global_search` resource for the full structure.")]
1696    pub async fn global_search_replace_all(&self, params: Parameters<GlobalSearchArgs>) -> Result<CallToolResult, McpError> {
1697        let search = parse_json!(&params.0.search);
1698        send_and_respond!(self, "global_search_replace_all", Command::GlobalSearchReplaceAll(params.0.pack_key, search))
1699    }
1700
1701    #[tool(description = "Find all references to a value in the pack identified by `pack_key`. The `reference_map` is a JSON object mapping table names to column name arrays, e.g. {\"land_units_tables\": [\"key\", \"unit\"]}. The `value` is the string to search for across those columns.")]
1702    pub async fn search_references(&self, params: Parameters<SearchReferencesArgs>) -> Result<CallToolResult, McpError> {
1703        let map: HashMap<String, Vec<String>> = parse_json!(&params.0.reference_map);
1704        send_and_respond!(self, "search_references", Command::SearchReferences(params.0.pack_key, map, params.0.value))
1705    }
1706
1707    #[tool(description = "Get valid reference values for columns in a table definition for the pack identified by `pack_key`. The `definition` is a Definition JSON (as returned by `get_table_definition_from_dependency_pack_file`). Set `force` to true to regenerate cached reference data.")]
1708    pub async fn get_reference_data_from_definition(&self, params: Parameters<GetReferenceDataFromDefinitionArgs>) -> Result<CallToolResult, McpError> {
1709        let def = parse_json!(&params.0.definition);
1710        send_and_respond!(self, "get_reference_data_from_definition", Command::GetReferenceDataFromDefinition(params.0.pack_key, params.0.table_name, def, params.0.force))
1711    }
1712
1713    #[tool(description = "Go to the definition of a reference in the pack identified by `pack_key`. Provide table name, column name, and values to search.")]
1714    pub async fn go_to_definition(&self, params: Parameters<GoToDefinitionArgs>) -> Result<CallToolResult, McpError> {
1715        send_and_respond!(self, "go_to_definition", Command::GoToDefinition(params.0.pack_key, params.0.table_name, params.0.column_name, params.0.values))
1716    }
1717
1718    #[tool(description = "Go to a loc key's location in the pack identified by `pack_key`.")]
1719    pub async fn go_to_loc(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1720        send_and_respond!(self, "go_to_loc", Command::GoToLoc(params.0.pack_key, params.0.value))
1721    }
1722
1723    #[tool(description = "Get the source data of a loc key in the pack identified by `pack_key`.")]
1724    pub async fn get_source_data_from_loc_key(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1725        send_and_respond!(self, "get_source_data_from_loc_key", Command::GetSourceDataFromLocKey(params.0.pack_key, params.0.value))
1726    }
1727
1728    //-----------------------------------------------------------------------//
1729    // Schema
1730    //-----------------------------------------------------------------------//
1731
1732    #[tool(description = "Save the provided schema to disk. The `schema` is the full Schema JSON object (as returned by `get_schema`). Use this after modifying definitions or applying patches.")]
1733    pub async fn save_schema(&self, params: Parameters<SaveSchemaArgs>) -> Result<CallToolResult, McpError> {
1734        let schema = parse_json!(&params.0.schema);
1735        send_and_respond!(self, "save_schema", Command::SaveSchema(schema))
1736    }
1737
1738    #[tool(description = "Update the currently loaded schema with data from the game's Assembly Kit.")]
1739    pub async fn update_current_schema_from_asskit(&self) -> Result<CallToolResult, McpError> {
1740        send_and_respond!(self, "update_current_schema_from_asskit", Command::UpdateCurrentSchemaFromAssKit)
1741    }
1742
1743    #[tool(description = "Update schemas from the remote repository.")]
1744    pub async fn update_schemas(&self) -> Result<CallToolResult, McpError> {
1745        send_and_respond!(self, "update_schemas", Command::UpdateSchemas)
1746    }
1747
1748    #[tool(description = "Check if a schema is currently loaded.")]
1749    pub async fn is_schema_loaded(&self) -> Result<CallToolResult, McpError> {
1750        send_and_respond!(self, "is_schema_loaded", Command::IsSchemaLoaded)
1751    }
1752
1753    #[tool(description = "Get the current schema.")]
1754    pub async fn get_schema(&self) -> Result<CallToolResult, McpError> {
1755        send_and_respond!(self, "get_schema", Command::Schema)
1756    }
1757
1758    #[tool(description = "Get all definitions for a table name. NOTE: the returned `fields` list is the raw on-disk field layout, not what row data looks like (e.g. colour columns are split into separate r/g/b fields here). Pass the definition to `fields_processed` to get the field list/count that rows must actually match.")]
1759    pub async fn definitions_by_table_name(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
1760        send_and_respond!(self, "definitions_by_table_name", Command::DefinitionsByTableName(params.0.value))
1761    }
1762
1763    #[tool(description = "Get a specific definition by table name and version. NOTE: the returned `fields` list is the raw on-disk field layout, not what row data looks like (e.g. colour columns are split into separate r/g/b fields here). Do not use `fields.len()` to size a row for saving — pass this definition to `fields_processed` first to get the field list/count and types that rows must actually match.")]
1764    pub async fn definition_by_table_name_and_version(&self, params: Parameters<StringI32Args>) -> Result<CallToolResult, McpError> {
1765        send_and_respond!(self, "definition_by_table_name_and_version", Command::DefinitionByTableNameAndVersion(params.0.name, params.0.version))
1766    }
1767
1768    #[tool(description = "Delete a definition by table name and version.")]
1769    pub async fn delete_definition(&self, params: Parameters<StringI32Args>) -> Result<CallToolResult, McpError> {
1770        send_and_respond!(self, "delete_definition", Command::DeleteDefinition(params.0.name, params.0.version))
1771    }
1772
1773    #[tool(description = "Get columns from other tables that reference the given table's definition. The `definition` is a Definition JSON (as returned by `get_table_definition_from_dependency_pack_file` or `definitions_by_table_name`).")]
1774    pub async fn referencing_columns_for_definition(&self, params: Parameters<ReferencingColumnsForDefinitionArgs>) -> Result<CallToolResult, McpError> {
1775        let def = parse_json!(&params.0.definition);
1776        send_and_respond!(self, "referencing_columns_for_definition", Command::ReferencingColumnsForDefinition(params.0.table_name, def))
1777    }
1778
1779    #[tool(description = "Get the processed fields from a definition, with bitwise expansion, enum conversions, and colour-group merging applied. Call this before building or validating row data: table rows must have exactly as many entries as `fields_processed` returns, NOT as many as the raw `fields` list on the Definition (e.g. `definition_by_table_name_and_version`/`definitions_by_table_name` return raw fields, where a colour split into r/g/b counts as 3 fields instead of the 1 merged field rows actually use). Saving a row built against the raw field count/types will fail. The `definition` is a Definition JSON (as returned by `get_table_definition_from_dependency_pack_file`).")]
1780    pub async fn fields_processed(&self, params: Parameters<DefinitionArg>) -> Result<CallToolResult, McpError> {
1781        let def = parse_json!(&params.0.definition);
1782        send_and_respond!(self, "fields_processed", Command::FieldsProcessed(def))
1783    }
1784
1785    #[tool(description = "Save local schema patches to customize column metadata without modifying the upstream schema. The `patches` is a JSON object mapping table names to DefinitionPatch objects, e.g. {\"land_units_tables\": {\"field_patches\": {...}}}.")]
1786    pub async fn save_local_schema_patch(&self, params: Parameters<SchemaPatchArgs>) -> Result<CallToolResult, McpError> {
1787        let patches = parse_json!(&params.0.patches);
1788        send_and_respond!(self, "save_local_schema_patch", Command::SaveLocalSchemaPatch(patches))
1789    }
1790
1791    #[tool(description = "Remove local schema patches for a table.")]
1792    pub async fn remove_local_schema_patches_for_table(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
1793        send_and_respond!(self, "remove_local_schema_patches_for_table", Command::RemoveLocalSchemaPatchesForTable(params.0.value))
1794    }
1795
1796    #[tool(description = "Remove local schema patches for a specific field in a table.")]
1797    pub async fn remove_local_schema_patches_for_table_and_field(&self, params: Parameters<SettingsSetStringArgs>) -> Result<CallToolResult, McpError> {
1798        send_and_respond!(self, "remove_local_schema_patches_for_table_and_field", Command::RemoveLocalSchemaPatchesForTableAndField(params.0.key, params.0.value))
1799    }
1800
1801    #[tool(description = "Import a schema patch from an external source. The `patches` is a JSON object mapping table names to DefinitionPatch objects (same format as `save_local_schema_patch`).")]
1802    pub async fn import_schema_patch(&self, params: Parameters<SchemaPatchArgs>) -> Result<CallToolResult, McpError> {
1803        let patches = parse_json!(&params.0.patches);
1804        send_and_respond!(self, "import_schema_patch", Command::ImportSchemaPatch(patches))
1805    }
1806
1807    //-----------------------------------------------------------------------//
1808    // Table Operations
1809    //-----------------------------------------------------------------------//
1810
1811    #[tool(description = "Merge multiple compatible tables into one in the pack identified by `pack_key`. The `paths` is a JSON array of ContainerPath for the tables to merge, e.g. [{\"File\": \"db/land_units_tables/table1\"}, {\"File\": \"db/land_units_tables/table2\"}]. The `merged_path` is the destination path. Set `delete_source` to true to remove the original files. DB Tables can also be enabled for Delta Merging (if two or more tables edit the same row, it merges their changes into a single row).")]
1812    pub async fn merge_files(&self, params: Parameters<MergeFilesArgs>) -> Result<CallToolResult, McpError> {
1813        let paths: Vec<ContainerPath> = parse_json!(&params.0.paths);
1814        let mut options = MergeOptions::default();
1815        options.set_delta_merge(params.0.delta_merge);
1816        send_and_respond!(self, "merge_files", Command::MergeFiles(params.0.pack_key, paths, params.0.merged_path, params.0.delete_source, options))
1817    }
1818
1819    #[tool(description = "Update a table to the latest schema version in the pack identified by `pack_key`. The `value` is a ContainerPath JSON, e.g. {\"File\": \"db/land_units_tables/my_mod\"}.")]
1820    pub async fn update_table(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1821        let path: ContainerPath = parse_json!(&params.0.value);
1822        send_and_respond!(self, "update_table", Command::UpdateTable(params.0.pack_key, path))
1823    }
1824
1825    #[tool(description = "Trigger a cascade edition on all referenced data in the pack identified by `pack_key`. When a key value changes, this propagates the change to all referencing tables. The `definition` is a Definition JSON for the source table. The `changes` is a JSON array of [field, old_value, new_value] tuples, e.g. [[field_json, \"old_key\", \"new_key\"]].")]
1826    pub async fn cascade_edition(&self, params: Parameters<CascadeEditionArgs>) -> Result<CallToolResult, McpError> {
1827        let def = parse_json!(&params.0.definition);
1828        let changes = parse_json!(&params.0.changes);
1829        send_and_respond!(self, "cascade_edition", Command::CascadeEdition(params.0.pack_key, params.0.table_name, def, changes))
1830    }
1831
1832    #[tool(description = "Get table paths by table name from the pack identified by `pack_key`.")]
1833    pub async fn get_tables_by_table_name(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1834        send_and_respond!(self, "get_tables_by_table_name", Command::GetTablesByTableName(params.0.pack_key, params.0.value))
1835    }
1836
1837    #[tool(description = "Add keys to the key_deletes table in the pack identified by `pack_key`.")]
1838    pub async fn add_keys_to_key_deletes(&self, params: Parameters<AddKeysToKeyDeletesArgs>) -> Result<CallToolResult, McpError> {
1839        send_and_respond!(self, "add_keys_to_key_deletes", Command::AddKeysToKeyDeletes(params.0.pack_key, params.0.table_file_name, params.0.key_table_name, params.0.keys))
1840    }
1841
1842    #[tool(description = "Export a table from the pack identified by `pack_key` to a TSV file.")]
1843    pub async fn export_tsv(&self, params: Parameters<TsvExportArgs>) -> Result<CallToolResult, McpError> {
1844        send_and_respond!(self, "export_tsv", Command::ExportTSV(params.0.pack_key, params.0.table_path, params.0.tsv_path, DataSource::PackFile))
1845    }
1846
1847    #[tool(description = "Import a TSV file to a table in the pack identified by `pack_key`.")]
1848    pub async fn import_tsv(&self, params: Parameters<TsvImportArgs>) -> Result<CallToolResult, McpError> {
1849        send_and_respond!(self, "import_tsv", Command::ImportTSV(params.0.pack_key, params.0.table_path, params.0.tsv_path))
1850    }
1851
1852    //-----------------------------------------------------------------------//
1853    // Diagnostics
1854    //-----------------------------------------------------------------------//
1855
1856    #[tool(description = "Run a full diagnostics check over all open packs.")]
1857    pub async fn diagnostics_check(&self, params: Parameters<DiagnosticsCheckArgs>) -> Result<CallToolResult, McpError> {
1858        send_and_respond!(self, "diagnostics_check", Command::DiagnosticsCheck(params.0.ignored, params.0.check_ak_only_refs))
1859    }
1860
1861    #[tool(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.
1862
1863Test file API (a global `rpfm` table):
1864- 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.
1865- `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.
1866- `rpfm.fire(event, { accessor = value, ... })` triggers an event, like `rpfm.fire(\"FactionTurnStart\", { faction = kislev })`.
1867- `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`.
1868- `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.
1869- `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`.
1870
1871The 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.")]
1872    pub async fn lua_run_tests(&self, params: Parameters<LuaRunTestsArgs>) -> Result<CallToolResult, McpError> {
1873        send_and_respond!(self, "lua_run_tests", Command::LuaRunTests(params.0.test_source, params.0.campaign))
1874    }
1875
1876    #[tool(description = "Update diagnostics incrementally for changed files across all open packs. The `diagnostics` is the Diagnostics JSON from a previous `diagnostics_check` call. The `paths` is a JSON array of ContainerPath for the files that changed, e.g. [{\"File\": \"db/land_units_tables/my_mod\"}].")]
1877    pub async fn diagnostics_update(&self, params: Parameters<DiagnosticsUpdateArgs>) -> Result<CallToolResult, McpError> {
1878        let diag = parse_json!(&params.0.diagnostics);
1879        let paths: Vec<ContainerPath> = parse_json!(&params.0.paths);
1880        send_and_respond!(self, "diagnostics_update", Command::DiagnosticsUpdate(diag, paths, params.0.check_ak_only_refs))
1881    }
1882
1883    #[tool(description = "Add a line to the ignored diagnostics list for the pack identified by `pack_key`.")]
1884    pub async fn add_line_to_pack_ignored_diagnostics(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1885        send_and_respond!(self, "add_line_to_pack_ignored_diagnostics", Command::AddLineToPackIgnoredDiagnostics(params.0.pack_key, params.0.value))
1886    }
1887
1888    #[tool(description = "Export missing table definitions for the pack identified by `pack_key` to a file (for debugging).")]
1889    pub async fn get_missing_definitions(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
1890        send_and_respond!(self, "get_missing_definitions", Command::GetMissingDefinitions(params.0.pack_key))
1891    }
1892
1893    //-----------------------------------------------------------------------//
1894    // Notes
1895    //-----------------------------------------------------------------------//
1896
1897    #[tool(description = "Get all notes under a path in the pack identified by `pack_key`.")]
1898    pub async fn notes_for_path(&self, params: Parameters<PackKeyStringArg>) -> Result<CallToolResult, McpError> {
1899        send_and_respond!(self, "notes_for_path", Command::NotesForPath(params.0.pack_key, params.0.value))
1900    }
1901
1902    #[tool(description = "Add a note to the pack identified by `pack_key`. The `note` is a Note JSON object with fields: path (string — the file or folder path to attach the note to), id (u64), text (string — the note content).")]
1903    pub async fn add_note(&self, params: Parameters<AddNoteArgs>) -> Result<CallToolResult, McpError> {
1904        let note = parse_json!(&params.0.note);
1905        send_and_respond!(self, "add_note", Command::AddNote(params.0.pack_key, note))
1906    }
1907
1908    #[tool(description = "Delete a note by path and ID in the pack identified by `pack_key`.")]
1909    pub async fn delete_note(&self, params: Parameters<DeleteNoteArgs>) -> Result<CallToolResult, McpError> {
1910        send_and_respond!(self, "delete_note", Command::DeleteNote(params.0.pack_key, params.0.path, params.0.id))
1911    }
1912
1913    //-----------------------------------------------------------------------//
1914    // Optimization
1915    //-----------------------------------------------------------------------//
1916
1917    #[tool(description = "Optimize the pack identified by `pack_key` by removing unchanged/duplicate data. The `options` is an OptimizerOptions JSON with boolean fields: pack_remove_itm_files, table_remove_duplicated_entries, table_remove_itm_entries, table_remove_itnr_entries, table_remove_empty_file, db_optimize_datacored_tables, etc. See the `rpfm://examples/optimizer_options` resource for all fields.")]
1918    pub async fn optimize_pack_file(&self, params: Parameters<OptimizePackFileArgs>) -> Result<CallToolResult, McpError> {
1919        let options = parse_json!(&params.0.options);
1920        send_and_respond!(self, "optimize_pack_file", Command::OptimizePackFile(params.0.pack_key, options))
1921    }
1922
1923    #[tool(description = "Get the default optimizer options.")]
1924    pub async fn get_optimizer_options(&self) -> Result<CallToolResult, McpError> {
1925        send_and_respond!(self, "get_optimizer_options", Command::OptimizerOptions)
1926    }
1927
1928    //-----------------------------------------------------------------------//
1929    // Updates
1930    //-----------------------------------------------------------------------//
1931
1932    #[tool(description = "Check if there is an RPFM update available.")]
1933    pub async fn check_updates(&self) -> Result<CallToolResult, McpError> {
1934        send_and_respond!(self, "check_updates", Command::CheckUpdates)
1935    }
1936
1937    #[tool(description = "Check if there is a schema update available.")]
1938    pub async fn check_schema_updates(&self) -> Result<CallToolResult, McpError> {
1939        send_and_respond!(self, "check_schema_updates", Command::CheckSchemaUpdates)
1940    }
1941
1942    #[tool(description = "Check for Lua autogen updates.")]
1943    pub async fn check_lua_autogen_updates(&self) -> Result<CallToolResult, McpError> {
1944        send_and_respond!(self, "check_lua_autogen_updates", Command::CheckLuaAutogenUpdates)
1945    }
1946
1947    #[tool(description = "Check for Empire/Napoleon Assembly Kit updates.")]
1948    pub async fn check_empire_and_napoleon_ak_updates(&self) -> Result<CallToolResult, McpError> {
1949        send_and_respond!(self, "check_empire_and_napoleon_ak_updates", Command::CheckEmpireAndNapoleonAKUpdates)
1950    }
1951
1952    #[tool(description = "Check for translation updates.")]
1953    pub async fn check_translations_updates(&self) -> Result<CallToolResult, McpError> {
1954        send_and_respond!(self, "check_translations_updates", Command::CheckTranslationsUpdates)
1955    }
1956
1957    #[tool(description = "Update the Lua autogen repository.")]
1958    pub async fn update_lua_autogen(&self) -> Result<CallToolResult, McpError> {
1959        send_and_respond!(self, "update_lua_autogen", Command::UpdateLuaAutogen)
1960    }
1961
1962    #[tool(description = "Update the program to the latest version.")]
1963    pub async fn update_main_program(&self) -> Result<CallToolResult, McpError> {
1964        send_and_respond!(self, "update_main_program", Command::UpdateMainProgram)
1965    }
1966
1967    #[tool(description = "Update the Empire/Napoleon Assembly Kit files.")]
1968    pub async fn update_empire_and_napoleon_ak(&self) -> Result<CallToolResult, McpError> {
1969        send_and_respond!(self, "update_empire_and_napoleon_ak", Command::UpdateEmpireAndNapoleonAK)
1970    }
1971
1972    #[tool(description = "Update the translations repository.")]
1973    pub async fn update_translations(&self) -> Result<CallToolResult, McpError> {
1974        send_and_respond!(self, "update_translations", Command::UpdateTranslations)
1975    }
1976
1977    //-----------------------------------------------------------------------//
1978    // Settings Getters
1979    //-----------------------------------------------------------------------//
1980
1981    #[tool(description = "Get a boolean setting value by key.")]
1982    pub async fn settings_get_bool(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
1983        send_and_respond!(self, "settings_get_bool", Command::SettingsGetBool(params.0.value))
1984    }
1985
1986    #[tool(description = "Get an i32 setting value by key.")]
1987    pub async fn settings_get_i32(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
1988        send_and_respond!(self, "settings_get_i32", Command::SettingsGetI32(params.0.value))
1989    }
1990
1991    #[tool(description = "Get an f32 setting value by key.")]
1992    pub async fn settings_get_f32(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
1993        send_and_respond!(self, "settings_get_f32", Command::SettingsGetF32(params.0.value))
1994    }
1995
1996    #[tool(description = "Get a string setting value by key.")]
1997    pub async fn settings_get_string(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
1998        send_and_respond!(self, "settings_get_string", Command::SettingsGetString(params.0.value))
1999    }
2000
2001    #[tool(description = "Get a PathBuf setting value by key.")]
2002    pub async fn settings_get_path_buf(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
2003        send_and_respond!(self, "settings_get_path_buf", Command::SettingsGetPathBuf(params.0.value))
2004    }
2005
2006    #[tool(description = "Get a Vec<String> setting value by key.")]
2007    pub async fn settings_get_vec_string(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
2008        send_and_respond!(self, "settings_get_vec_string", Command::SettingsGetVecString(params.0.value))
2009    }
2010
2011    #[tool(description = "Get a raw bytes setting value by key.")]
2012    pub async fn settings_get_vec_raw(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
2013        send_and_respond!(self, "settings_get_vec_raw", Command::SettingsGetVecRaw(params.0.value))
2014    }
2015
2016    #[tool(description = "Get all settings at once (bool, i32, f32, string, raw_data, and vec_string maps).")]
2017    pub async fn settings_get_all(&self) -> Result<CallToolResult, McpError> {
2018        send_and_respond!(self, "settings_get_all", Command::SettingsGetAll)
2019    }
2020
2021    //-----------------------------------------------------------------------//
2022    // Settings Setters
2023    //-----------------------------------------------------------------------//
2024
2025    #[tool(description = "Set a boolean setting value.")]
2026    pub async fn settings_set_bool(&self, params: Parameters<SettingsSetBoolArgs>) -> Result<CallToolResult, McpError> {
2027        send_and_respond!(self, "settings_set_bool", Command::SettingsSetBool(params.0.key, params.0.value))
2028    }
2029
2030    #[tool(description = "Set an i32 setting value.")]
2031    pub async fn settings_set_i32(&self, params: Parameters<SettingsSetI32Args>) -> Result<CallToolResult, McpError> {
2032        send_and_respond!(self, "settings_set_i32", Command::SettingsSetI32(params.0.key, params.0.value))
2033    }
2034
2035    #[tool(description = "Set an f32 setting value.")]
2036    pub async fn settings_set_f32(&self, params: Parameters<SettingsSetF32Args>) -> Result<CallToolResult, McpError> {
2037        send_and_respond!(self, "settings_set_f32", Command::SettingsSetF32(params.0.key, params.0.value))
2038    }
2039
2040    #[tool(description = "Set a string setting value.")]
2041    pub async fn settings_set_string(&self, params: Parameters<SettingsSetStringArgs>) -> Result<CallToolResult, McpError> {
2042        send_and_respond!(self, "settings_set_string", Command::SettingsSetString(params.0.key, params.0.value))
2043    }
2044
2045    #[tool(description = "Set a PathBuf setting value.")]
2046    pub async fn settings_set_path_buf(&self, params: Parameters<SettingsSetPathBufArgs>) -> Result<CallToolResult, McpError> {
2047        send_and_respond!(self, "settings_set_path_buf", Command::SettingsSetPathBuf(params.0.key, params.0.value))
2048    }
2049
2050    #[tool(description = "Set a Vec<String> setting value.")]
2051    pub async fn settings_set_vec_string(&self, params: Parameters<SettingsSetVecStringArgs>) -> Result<CallToolResult, McpError> {
2052        send_and_respond!(self, "settings_set_vec_string", Command::SettingsSetVecString(params.0.key, params.0.value))
2053    }
2054
2055    #[tool(description = "Set a raw bytes setting value.")]
2056    pub async fn settings_set_vec_raw(&self, params: Parameters<SettingsSetVecRawArgs>) -> Result<CallToolResult, McpError> {
2057        send_and_respond!(self, "settings_set_vec_raw", Command::SettingsSetVecRaw(params.0.key, params.0.value))
2058    }
2059
2060    #[tool(description = "Backup the current settings to memory.")]
2061    pub async fn backup_settings(&self) -> Result<CallToolResult, McpError> {
2062        send_and_respond!(self, "backup_settings", Command::BackupSettings)
2063    }
2064
2065    #[tool(description = "Clear all settings and reset to defaults.")]
2066    pub async fn clear_settings(&self) -> Result<CallToolResult, McpError> {
2067        send_and_respond!(self, "clear_settings", Command::ClearSettings)
2068    }
2069
2070    #[tool(description = "Restore settings from the backup.")]
2071    pub async fn restore_backup_settings(&self) -> Result<CallToolResult, McpError> {
2072        send_and_respond!(self, "restore_backup_settings", Command::RestoreBackupSettings)
2073    }
2074
2075    //-----------------------------------------------------------------------//
2076    // Path Queries
2077    //-----------------------------------------------------------------------//
2078
2079    #[tool(description = "Get the config path.")]
2080    pub async fn config_path(&self) -> Result<CallToolResult, McpError> {
2081        send_and_respond!(self, "config_path", Command::ConfigPath)
2082    }
2083
2084    #[tool(description = "Get the Assembly Kit path for the current game.")]
2085    pub async fn assembly_kit_path(&self) -> Result<CallToolResult, McpError> {
2086        send_and_respond!(self, "assembly_kit_path", Command::AssemblyKitPath)
2087    }
2088
2089    #[tool(description = "Get the backup autosave path.")]
2090    pub async fn backup_autosave_path(&self) -> Result<CallToolResult, McpError> {
2091        send_and_respond!(self, "backup_autosave_path", Command::BackupAutosavePath)
2092    }
2093
2094    #[tool(description = "Get the old Assembly Kit data path.")]
2095    pub async fn old_ak_data_path(&self) -> Result<CallToolResult, McpError> {
2096        send_and_respond!(self, "old_ak_data_path", Command::OldAkDataPath)
2097    }
2098
2099    #[tool(description = "Get the schemas path.")]
2100    pub async fn schemas_path(&self) -> Result<CallToolResult, McpError> {
2101        send_and_respond!(self, "schemas_path", Command::SchemasPath)
2102    }
2103
2104    #[tool(description = "Get the table profiles path.")]
2105    pub async fn table_profiles_path(&self) -> Result<CallToolResult, McpError> {
2106        send_and_respond!(self, "table_profiles_path", Command::TableProfilesPath)
2107    }
2108
2109    #[tool(description = "Get the translations local path.")]
2110    pub async fn translations_local_path(&self) -> Result<CallToolResult, McpError> {
2111        send_and_respond!(self, "translations_local_path", Command::TranslationsLocalPath)
2112    }
2113
2114    #[tool(description = "Get the dependencies cache path.")]
2115    pub async fn dependencies_cache_path(&self) -> Result<CallToolResult, McpError> {
2116        send_and_respond!(self, "dependencies_cache_path", Command::DependenciesCachePath)
2117    }
2118
2119    #[tool(description = "Clear a config path.")]
2120    pub async fn settings_clear_path(&self, params: Parameters<PathArg>) -> Result<CallToolResult, McpError> {
2121        send_and_respond!(self, "settings_clear_path", Command::SettingsClearPath(params.0.path))
2122    }
2123
2124    //-----------------------------------------------------------------------//
2125    // Specialized
2126    //-----------------------------------------------------------------------//
2127
2128    #[tool(description = "Get the info about the pack identified by `pack_key` and the list of files it contains.")]
2129    pub async fn open_pack_info(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
2130        send_and_respond!(self, "open_pack_info", Command::GetPackFileDataForTreeView(params.0.pack_key))
2131    }
2132
2133    #[tool(description = "Initialize a MyMod folder for mod development.")]
2134    pub async fn initialize_my_mod_folder(&self, params: Parameters<InitializeMyModFolderArgs>) -> Result<CallToolResult, McpError> {
2135        send_and_respond!(self, "initialize_my_mod_folder", Command::InitializeMyModFolder(params.0.name, params.0.game, params.0.sublime, params.0.vscode, params.0.gitignore))
2136    }
2137
2138    #[tool(description = "Live export the pack identified by `pack_key` to the game folder for testing.")]
2139    pub async fn live_export(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
2140        send_and_respond!(self, "live_export", Command::LiveExport(params.0.pack_key))
2141    }
2142
2143    #[tool(description = "Patch the SiegeAI of a Siege Map in the pack identified by `pack_key` for Warhammer games.")]
2144    pub async fn patch_siege_ai(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
2145        send_and_respond!(self, "patch_siege_ai", Command::PatchSiegeAI(params.0.pack_key))
2146    }
2147
2148    #[tool(description = "Pack map tiles into the pack identified by `pack_key`. The `tile_maps` is a list of tile map file paths on disk. The `tiles` is a JSON array of [path, name] pairs, e.g. [[\"/path/to/tile\", \"tile_name\"]].")]
2149    pub async fn pack_map(&self, params: Parameters<PackMapArgs>) -> Result<CallToolResult, McpError> {
2150        let tiles: Vec<(PathBuf, String)> = parse_json!(&params.0.tiles);
2151        send_and_respond!(self, "pack_map", Command::PackMap(params.0.pack_key, params.0.tile_maps, tiles))
2152    }
2153
2154    #[tool(description = "Generate all missing loc entries for the pack identified by `pack_key`.")]
2155    pub async fn generate_missing_loc_data(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
2156        send_and_respond!(self, "generate_missing_loc_data", Command::GenerateMissingLocData(params.0.pack_key))
2157    }
2158
2159    #[tool(description = "Get pack translation data for a language from the pack identified by `pack_key`.")]
2160    pub async fn get_pack_translation(&self, params: Parameters<GetPackTranslationArgs>) -> Result<CallToolResult, McpError> {
2161        send_and_respond!(self, "get_pack_translation", Command::GetPackTranslation(params.0.pack_key, params.0.src_lang, params.0.language))
2162    }
2163
2164    #[tool(description = "Generate the vanilla texts of a source language from the game's locale packs. Returns whether vanilla texts for that language are available.")]
2165    pub async fn generate_vanilla_translation_source(&self, params: Parameters<SrcLangArg>) -> Result<CallToolResult, McpError> {
2166        send_and_respond!(self, "generate_vanilla_translation_source", Command::GenerateVanillaTranslationSource(params.0.src_lang))
2167    }
2168
2169    #[tool(description = "Get campaign IDs for starpos building in the pack identified by `pack_key`.")]
2170    pub async fn build_starpos_get_campaign_ids(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
2171        send_and_respond!(self, "build_starpos_get_campaign_ids", Command::BuildStarposGetCampaingIds(params.0.pack_key))
2172    }
2173
2174    #[tool(description = "Check if victory conditions file exists for starpos building in the pack identified by `pack_key`.")]
2175    pub async fn build_starpos_check_victory_conditions(&self, params: Parameters<PackKeyArg>) -> Result<CallToolResult, McpError> {
2176        send_and_respond!(self, "build_starpos_check_victory_conditions", Command::BuildStarposCheckVictoryConditions(params.0.pack_key))
2177    }
2178
2179    #[tool(description = "Build starpos (pre-processing step) for the pack identified by `pack_key`.")]
2180    pub async fn build_starpos(&self, params: Parameters<BuildStarposArgs>) -> Result<CallToolResult, McpError> {
2181        send_and_respond!(self, "build_starpos", Command::BuildStarpos(params.0.pack_key, params.0.campaign_id, params.0.process_hlp_spd))
2182    }
2183
2184    #[tool(description = "Build starpos (post-processing step) for the pack identified by `pack_key`.")]
2185    pub async fn build_starpos_post(&self, params: Parameters<BuildStarposArgs>) -> Result<CallToolResult, McpError> {
2186        send_and_respond!(self, "build_starpos_post", Command::BuildStarposPost(params.0.pack_key, params.0.campaign_id, params.0.process_hlp_spd))
2187    }
2188
2189    #[tool(description = "Clean up starpos temporary files for the pack identified by `pack_key`.")]
2190    pub async fn build_starpos_cleanup(&self, params: Parameters<BuildStarposArgs>) -> Result<CallToolResult, McpError> {
2191        send_and_respond!(self, "build_starpos_cleanup", Command::BuildStarposCleanup(params.0.pack_key, params.0.campaign_id, params.0.process_hlp_spd))
2192    }
2193
2194    #[tool(description = "Update animation IDs with an offset in the pack identified by `pack_key`.")]
2195    pub async fn update_anim_ids(&self, params: Parameters<UpdateAnimIdsArgs>) -> Result<CallToolResult, McpError> {
2196        send_and_respond!(self, "update_anim_ids", Command::UpdateAnimIds(params.0.pack_key, params.0.starting_id, params.0.offset))
2197    }
2198
2199    #[tool(description = "Get animation paths by skeleton name.")]
2200    pub async fn get_anim_paths_by_skeleton_name(&self, params: Parameters<StringArg>) -> Result<CallToolResult, McpError> {
2201        send_and_respond!(self, "get_anim_paths_by_skeleton_name", Command::GetAnimPathsBySkeletonName(params.0.value))
2202    }
2203
2204    #[tool(description = "Export a RigidModel to glTF format. The `rigid_model` is a RigidModel JSON object (as returned by decoding a .rigid_model_v2 file with `decode_packed_file`). The `output_path` is the destination file path on disk.")]
2205    pub async fn export_rigid_to_gltf(&self, params: Parameters<ExportRigidToGltfArgs>) -> Result<CallToolResult, McpError> {
2206        let rigid = parse_json!(&params.0.rigid_model);
2207        send_and_respond!(self, "export_rigid_to_gltf", Command::ExportRigidToGltf(rigid, params.0.output_path))
2208    }
2209
2210    #[tool(description = "Change the format of a ca_vp8 video file in the pack identified by `pack_key`. Valid formats: \"CaVp8\" (CA custom VP8) or \"Ivf\" (standard VP8 IVF).")]
2211    pub async fn set_video_format(&self, params: Parameters<SetVideoFormatArgs>) -> Result<CallToolResult, McpError> {
2212        let format = parse_json!(&params.0.format);
2213        send_and_respond!(self, "set_video_format", Command::SetVideoFormat(params.0.pack_key, params.0.path, format))
2214    }
2215
2216    //-----------------------------------------------------------------------//
2217    // Multi-Pack Management
2218    //-----------------------------------------------------------------------//
2219
2220    #[tool(description = "List all currently open packs with their keys and metadata. Use this to get valid pack_key values for other tools.")]
2221    pub async fn list_open_packs(&self) -> Result<CallToolResult, McpError> {
2222        send_and_respond!(self, "list_open_packs", Command::ListOpenPacks)
2223    }
2224
2225    //-----------------------------------------------------------------------//
2226    // Additional tools
2227    //-----------------------------------------------------------------------//
2228
2229    #[tool(description = "Close all currently open packs without saving. Any unsaved changes will be lost.")]
2230    pub async fn close_all_packs(&self) -> Result<CallToolResult, McpError> {
2231        send_and_respond!(self, "close_all_packs", Command::CloseAllPacks)
2232    }
2233
2234}
2235
2236//-------------------------------------------------------------------------------//
2237//                              MCP Prompts
2238//-------------------------------------------------------------------------------//
2239
2240#[prompt_router]
2241impl McpServer {
2242
2243    #[prompt(name = "open_and_inspect_pack", description = "Walk through opening a PackFile and inspecting its contents.")]
2244    pub async fn open_and_inspect_pack(&self) -> Vec<PromptMessage> {
2245        vec![PromptMessage::new_text(
2246            Role::User,
2247            "\
2248You are an assistant helping the user inspect a Total War PackFile using the RPFM MCP server.
2249
2250Follow these steps in order:
2251
22521. **Open the pack** – Call `open_packfiles` with the filesystem path(s) the user provides.
2253   The response contains one or more pack keys; remember them for subsequent calls.
2254
22552. **Select the game** – Call `set_game_selected` with the correct game key (e.g. `\"warhammer_3\"`)
2256   and `rebuild_dependencies: true` so that schemas and dependency data are loaded.
2257
22583. **List pack contents** – Call `open_pack_info` with the pack key to get the full file tree.
2259   Present the tree to the user in a readable format.
2260
22614. **Decode specific files** – When the user asks about a file, call `decode_packed_file` with the
2262   pack key, the internal path (e.g. `\"db/land_units_tables/my_table\"`), and
2263   `source: \"PackFile\"`. The decoded JSON will contain the table rows, schema, etc.
2264
22655. **Inspect metadata** – Use `get_pack_settings`, `get_pack_file_name`, or
2266   `get_dependency_pack_files_list` to answer questions about the pack itself.
2267
2268Important notes:
2269- Always call `list_open_packs` if you are unsure which pack key to use.
2270- If a file fails to decode, check `is_schema_loaded`; if false, call `update_schemas` first.
2271- When done, optionally call `close_pack` to free resources.
2272",
2273        )]
2274    }
2275
2276    #[prompt(name = "edit_db_table", description = "Guide for reading, modifying, and saving a DB table inside a pack.")]
2277    pub async fn edit_db_table(&self) -> Vec<PromptMessage> {
2278        vec![PromptMessage::new_text(
2279            Role::User,
2280            "\
2281You are an assistant helping the user edit a DB table inside a Total War PackFile.
2282
2283Workflow:
2284
22851. **Open the pack** – `open_packfiles` → note the `pack_key`.
22862. **Set the game** – `set_game_selected` with `rebuild_dependencies: true`.
22873. **Decode the table** – `decode_packed_file` with the DB path
2288   (e.g. `\"db/unit_stats_land_tables/my_table\"`) and `source: \"PackFile\"`.
2289   The response is an `RFileDecoded` JSON containing the table data and definition.
22904. **Modify rows** – Edit the decoded JSON: add, remove, or change rows/cells.
2291   Each row is typically a list of `DecodedData` values matching the table's
2292   fields processed list (retrievable via the `FieldsProcessed` message).
22935. **Save back** – Call `save_packed_file_from_view` with the pack key, the same path,
2294   and the modified `RFileDecoded` JSON as the `data` parameter.
22956. **Save the pack** – Call `save_packfile` (or `save_pack_as` for a new path).
2296
2297Tips:
2298- Use `get_table_definition_from_dependency_pack_file` to see the table's definition, but
2299  always run it through `fields_processed` before using its field list/count — the
2300  definition's raw `fields` do NOT match row shape (e.g. a colour column is split into
2301  separate r/g/b fields there); rows must match `fields_processed` exactly or saving
2302  will fail with a field-count or type error.
2303- Use `get_reference_data_from_definition` to discover valid values for referenced columns.
2304- After saving, you can run `diagnostics_check` to validate the pack.
2305",
2306        )]
2307    }
2308
2309    #[prompt(name = "create_new_mod", description = "Step-by-step guide for creating a new mod PackFile from scratch.")]
2310    pub async fn create_new_mod(&self) -> Vec<PromptMessage> {
2311        vec![PromptMessage::new_text(
2312            Role::User,
2313            "\
2314You are an assistant helping the user create a new Total War mod from scratch.
2315
2316Workflow:
2317
23181. **Set the game** – `set_game_selected` with the target game key and
2319   `rebuild_dependencies: true`.
2320
23212. **Create the pack** – `new_pack` returns a new empty pack and its pack key.
2322
23233. **Set pack type** – `set_pack_file_type` to `\"Mod\"` (the standard type for mods).
2324
23254. **Add DB tables** – For each table you need:
2326   a. Call `new_packed_file` with the pack key, the path (e.g. `\"db/land_units_tables/my_mod\"`),
2327      and the `new_file` JSON set to `\"DB\"` with the table name.
2328   b. Decode, edit, and save as described in the `edit_db_table` workflow.
2329
23305. **Add Loc files** – For localisation:
2331   a. `new_packed_file` with path `\"text/db/my_mod.loc\"` and `new_file` set to `\"Loc\"`.
2332   b. Decode, add key/value rows, and save.
2333
23346. **Add other files** – Use `add_packed_files` to import assets from disk (images, models, etc.).
2335
23367. **Save the pack** – `save_pack_as` to write the final `.pack` file to disk.
2337
2338Optional steps:
2339- `initialize_my_mod_folder` to set up a mod development folder with IDE support.
2340- `optimize_pack_file` to strip unchanged rows that match vanilla data.
2341- `diagnostics_check` to validate everything before release.
2342",
2343        )]
2344    }
2345
2346    #[prompt(name = "search_and_replace", description = "Find and replace values across all files in a pack.")]
2347    pub async fn search_and_replace(&self) -> Vec<PromptMessage> {
2348        vec![PromptMessage::new_text(
2349            Role::User,
2350            "\
2351You are an assistant helping the user search for and replace data across a PackFile.
2352
2353Workflow:
2354
23551. **Open the pack** and **set the game** (see `open_and_inspect_pack` prompt).
2356
23572. **Run a global search** – Call `global_search` with the pack key and a `GlobalSearch`
2358   JSON object. The search object specifies the pattern, whether to use regex, which file
2359   types to include (DB, Loc, Text), and the replacement string.
2360
23613. **Review matches** – The response contains all matches grouped by file.
2362   Present them to the user for review.
2363
23644. **Replace selectively** – Call `global_search_replace_matches` with the same search
2365   object and a `Vec<MatchHolder>` containing only the matches the user approved.
2366
23675. **Or replace all** – If the user confirms a blanket replace, call
2368   `global_search_replace_all` with the search object.
2369
23706. **Save** – `save_packfile` to persist changes.
2371
2372Related tools:
2373- `search_references` – Find all rows that reference a specific value across tables.
2374- `go_to_definition` – Jump to where a referenced key is defined.
2375- `go_to_loc` – Find the loc entry for a given key.
2376",
2377        )]
2378    }
2379
2380    #[prompt(name = "manage_dependencies", description = "Set up and work with game dependencies and vanilla data.")]
2381    pub async fn manage_dependencies(&self) -> Vec<PromptMessage> {
2382        vec![PromptMessage::new_text(
2383            Role::User,
2384            "\
2385You are an assistant helping the user work with dependency data (vanilla game files).
2386
2387Workflow:
2388
23891. **Set the game** – `set_game_selected` with `rebuild_dependencies: true`.
2390
23912. **Check dependency database** – `is_there_a_dependency_database` with `true` to verify
2392   that game data (including Assembly Kit data) is loaded.
2393   If it returns false, call `generate_dependencies_cache` first.
2394
23953. **Browse vanilla tables** – `get_table_list_from_dependency_pack_file` returns all
2396   DB table names from the vanilla game files.
2397
23984. **Read vanilla data** – `get_tables_from_dependencies` with a table name to get
2399   all rows from vanilla for that table.
2400
24015. **Get definitions** – `get_table_definition_from_dependency_pack_file` to get the
2402   schema definition for any table.
2403
24046. **Import from vanilla** – `import_dependencies_to_open_pack_file` to copy specific
2405   files from vanilla into your mod pack.
2406
24077. **Open CA packs** – `load_all_ca_pack_files` opens all vanilla packs as one merged
2408   read-only pack for full browsing.
2409
24108. **Cross-source lookups** – `get_rfiles_from_all_sources` retrieves files by path
2411   from PackFile, GameFiles, and ParentFiles simultaneously.
2412
2413Tips:
2414- Use `get_packed_files_names_starting_with_path_from_all_sources` to discover files
2415  under a given path prefix across all sources.
2416- `set_dependency_pack_files_list` lets you mark other mods as dependencies of your pack.
2417- The `definition` bundled in each file from `get_tables_from_dependencies` (and from
2418  `get_table_definition_from_dependency_pack_file`) lists RAW on-disk fields, which can
2419  have a different length/order than the actual decoded rows (e.g. colour columns are
2420  split into separate r/g/b fields there). Run it through `fields_processed` before
2421  matching it up against row cells or reusing it to build new rows.
2422",
2423        )]
2424    }
2425
2426    #[prompt(name = "run_diagnostics", description = "Validate a pack and fix common issues.")]
2427    pub async fn run_diagnostics(&self) -> Vec<PromptMessage> {
2428        vec![PromptMessage::new_text(
2429            Role::User,
2430            "\
2431You are an assistant helping the user validate a Total War mod PackFile.
2432
2433Workflow:
2434
24351. **Open the pack** and **set the game** with `rebuild_dependencies: true`.
2436
24372. **Generate dependencies** – If dependencies have not been generated yet,
2438   call `generate_dependencies` to build the dependency data needed for diagnostics.
2439
24403. **Run full diagnostics** – `diagnostics_check` with an empty `ignored` list
2441   and `check_ak_only_refs: false` (or `true` to include Assembly Kit references).
2442   The response contains all warnings and errors grouped by category.
2443
24444. **Review results** – Present the diagnostic results to the user, grouped by severity.
2445   Common issues include:
2446   - Invalid references (a column references a key that does not exist)
2447   - Duplicate keys
2448   - Empty loc entries
2449   - Outdated table versions
2450
24515. **Fix issues** – For each issue:
2452   - Decode the affected file with `decode_packed_file`.
2453   - Apply the fix (correct a reference, remove a duplicate row, etc.).
2454   - Save with `save_packed_file_from_view`.
2455
24566. **Ignore false positives** – Use `add_line_to_pack_ignored_diagnostics` to suppress
2457   specific diagnostic lines that are intentional.
2458
24597. **Re-check** – After fixes, call `diagnostics_check` again to confirm all issues
2460   are resolved.
2461
24628. **Optimize** – Optionally run `optimize_pack_file` to remove rows that are identical
2463   to vanilla, reducing pack size.
2464",
2465        )]
2466    }
2467
2468    #[prompt(name = "schema_operations", description = "Work with table schemas: inspect, update, and patch definitions.")]
2469    pub async fn schema_operations(&self) -> Vec<PromptMessage> {
2470        vec![PromptMessage::new_text(
2471            Role::User,
2472            "\
2473You are an assistant helping the user manage RPFM table schemas.
2474
2475Workflow:
2476
24771. **Check schema status** – `is_schema_loaded` to verify a schema is loaded.
2478   If not, call `update_schemas` to download the latest from the repository.
2479
24802. **Get the full schema** – `get_schema` returns the entire schema object.
2481
24823. **Inspect a table definition** – `definitions_by_table_name` with a table name
2483   returns all known versions. Use `definition_by_table_name_and_version` for a
2484   specific version.
2485
24864. **See processed fields** – `fields_processed` takes a Definition JSON and returns
2487   fields with bitwise expansion, enum conversions, and colour-group merging applied.
2488   This is required, not just cosmetic: `definitions_by_table_name` and
2489   `definition_by_table_name_and_version` return the raw on-disk field list (e.g. a
2490   colour column split into separate r/g/b fields), which has a different length/order
2491   than actual row data. Always call `fields_processed` before using a definition's
2492   field list/count to build or validate rows for saving.
2493
24945. **Find referencing columns** – `referencing_columns_for_definition` shows which
2495   other tables reference a given table's columns.
2496
24976. **Patch a definition** – To customise column metadata (descriptions, references,
2498   default values) without modifying the upstream schema:
2499   a. Build a `HashMap<String, DefinitionPatch>` with your changes.
2500   b. Call `save_local_schema_patch` to persist it locally.
2501   c. Use `remove_local_schema_patches_for_table` or
2502      `remove_local_schema_patches_for_table_and_field` to undo patches.
2503
25047. **Import patches** – `import_schema_patch` applies a patch from another source.
2505
25068. **Update from Assembly Kit** – `update_current_schema_from_asskit` merges
2507   definition data from the game's Assembly Kit into the loaded schema.
2508
25099. **Save the schema** – `save_schema` writes the current in-memory schema to disk.
2510",
2511        )]
2512    }
2513
2514    #[prompt(name = "file_operations", description = "Add, remove, rename, extract, and move files within packs.")]
2515    pub async fn file_operations(&self) -> Vec<PromptMessage> {
2516        vec![PromptMessage::new_text(
2517            Role::User,
2518            "\
2519You are an assistant helping the user manage files inside a Total War PackFile.
2520
2521Common operations:
2522
2523**Add files from disk:**
2524- `add_packed_files` – Import files from the filesystem into the pack. Provide source
2525  filesystem paths and destination `ContainerPath` entries as JSON.
2526
2527**Add files from another pack:**
2528- `add_packed_files_from_pack_file` – Copy files between two open packs.
2529
2530**Create new files:**
2531- `new_packed_file` – Create a blank DB table, Loc file, or other file type inside the pack.
2532
2533**Delete files:**
2534- `delete_packed_files` – Remove files by their `ContainerPath` list.
2535
2536**Rename / move files:**
2537- `rename_packed_files` – Pass a list of `(old_path, new_path)` tuples.
2538
2539**Copy / Cut / Paste / Duplicate:**
2540- `copy_packed_files` – Copy files to the internal clipboard for later pasting.
2541- `cut_packed_files` – Cut files to the internal clipboard (removed from source on paste).
2542- `paste_packed_files` – Paste clipboard contents into a pack at the given folder path.
2543- `duplicate_packed_files` – Clone files in-place with a numeric suffix.
2544
2545**Extract to disk:**
2546- `extract_packed_files` – Export files from the pack to a folder on disk.
2547  Set `export_as_tsv: true` to export tables as TSV files.
2548
2549**AnimPack operations:**
2550- `add_packed_files_from_pack_file_to_animpack` – Add files to an AnimPack.
2551- `add_packed_files_from_animpack` – Extract files from an AnimPack.
2552- `delete_from_animpack` – Remove files from an AnimPack.
2553
2554**File info:**
2555- `get_packed_files_info` / `get_rfile_info` – Get metadata about files.
2556- `folder_exists` / `packed_file_exists` – Check if a path exists.
2557- `get_packed_file_raw_data` – Get the raw binary content of a file.
2558
2559**Merge tables:**
2560- `merge_files` – Combine multiple compatible tables into one.
2561
2562**External editing:**
2563- `open_packed_file_in_external_program` – Open a file in the system's default editor.
2564- `save_packed_file_from_external_view` – Re-import after external editing.
2565
2566Always call `save_packfile` or `save_pack_as` when done to persist changes.
2567",
2568        )]
2569    }
2570
2571    #[prompt(name = "troubleshooting", description = "Diagnose and fix common issues with RPFM and PackFiles.")]
2572    pub async fn troubleshooting(&self) -> Vec<PromptMessage> {
2573        vec![PromptMessage::new_text(
2574            Role::User,
2575            "\
2576You are an assistant helping the user troubleshoot common RPFM and PackFile issues.
2577
2578## Common Issues and Solutions
2579
2580### 1. Schema not loaded
2581**Symptom**: Files fail to decode, or `decode_packed_file` returns raw data.
2582**Solution**:
2583- Call `is_schema_loaded()` – if false, call `update_schemas()`.
2584- Make sure `set_game_selected` was called with `rebuild_dependencies: true`.
2585
2586### 2. Dependencies not available
2587**Symptom**: References show as invalid, diagnostics report missing keys.
2588**Solution**:
2589- Call `is_there_a_dependency_database(true)` – if false, call `generate_dependencies_cache()`.
2590- Ensure the game path is configured correctly in settings.
2591
2592### 3. Pack won't save
2593**Symptom**: `save_packfile` returns an error.
2594**Solution**:
2595- Check if the file is read-only or locked by another process.
2596- Try `save_pack_as` to a different path.
2597- As a last resort, use `clean_and_save_pack_as` to recover from corruption.
2598
2599### 4. Table version mismatch
2600**Symptom**: Table data looks wrong or has missing columns after a game update.
2601**Solution**:
2602- Call `update_schemas()` to get the latest table definitions.
2603- Use `update_table` to migrate the table to the current version.
2604- Check `get_table_definition_from_dependency_pack_file` for the expected schema.
2605
2606### 5. Wrong game selected
2607**Symptom**: Tables decode with wrong columns or fail to decode, dependencies are for a different game.
2608**Solution**:
2609- Call `get_game_selected()` to verify the current game.
2610- Call `set_game_selected` with the correct game key and `rebuild_dependencies: true`.
2611
2612### 6. Diagnostics show many reference errors
2613**Symptom**: `diagnostics_check` reports hundreds of invalid references.
2614**Solution**:
2615- Ensure dependencies are loaded (`is_there_a_dependency_database(true)`).
2616- Check if the pack depends on other mods via `get_dependency_pack_files_list`.
2617- Some references are Assembly Kit only; re-run with `check_ak_only_refs: true`.
2618- Use `add_line_to_pack_ignored_diagnostics` for intentional deviations.
2619
2620### Diagnostic Tools
2621- `diagnostics_check` – Full pack validation.
2622- `get_game_selected` – Verify game context.
2623- `is_schema_loaded` – Check schema status.
2624- `is_there_a_dependency_database` – Check dependency database status.
2625- `list_open_packs` – Verify which packs are open.
2626- `config_path` / `schemas_path` – Verify RPFM paths.
2627",
2628        )]
2629    }
2630
2631    #[prompt(name = "tsv_workflow", description = "Import and export tables as TSV files for batch editing in spreadsheets.")]
2632    pub async fn tsv_workflow(&self) -> Vec<PromptMessage> {
2633        vec![PromptMessage::new_text(
2634            Role::User,
2635            "\
2636You are an assistant helping the user work with TSV (Tab-Separated Values) files for batch editing \
2637Total War mod data in spreadsheets.
2638
2639## Export Workflow (Pack → TSV → Spreadsheet)
2640
26411. **Open the pack** and **set the game** with `rebuild_dependencies: true`.
2642
26432. **Export a single table as TSV**:
2644   Call `export_tsv` with:
2645   - `pack_key`: the pack key
2646   - `tsv_path`: destination path on disk (e.g. `/home/user/my_table.tsv`)
2647   - `table_path`: the internal path (e.g. `db/land_units_tables/my_mod`)
2648
26493. **Export all tables as TSV**:
2650   Call `extract_packed_files` with `export_as_tsv: true`.
2651   This exports all tables in the pack as TSV files to the destination folder.
2652
26534. **Edit in a spreadsheet**: Open the TSV file in LibreOffice Calc, Excel, or Google Sheets.
2654   - Keep the header rows intact (they contain schema metadata).
2655   - Tab-separated values — do not change the delimiter.
2656
2657## Import Workflow (Spreadsheet → TSV → Pack)
2658
26591. **Save the spreadsheet as TSV** (tab-delimited, UTF-8 encoding).
2660
26612. **Import the TSV back**:
2662   Call `import_tsv` with:
2663   - `pack_key`: the target pack key
2664   - `tsv_path`: path to the TSV file on disk
2665   - `table_path`: the internal path where the table should go
2666
26673. **Verify**: Call `decode_packed_file` to confirm the data imported correctly.
2668
26694. **Save the pack**: Call `save_packfile` to persist changes.
2670
2671## Tips
2672- TSV files include metadata headers that RPFM uses for schema matching.
2673  Do not delete or modify these header rows.
2674- Use `get_table_definition_from_dependency_pack_file` to understand column types
2675  before editing.
2676- After import, run `diagnostics_check` to validate references.
2677",
2678        )]
2679    }
2680
2681    #[prompt(name = "translation_workflow", description = "Work with localisation and translation data in PackFiles.")]
2682    pub async fn translation_workflow(&self) -> Vec<PromptMessage> {
2683        vec![PromptMessage::new_text(
2684            Role::User,
2685            "\
2686You are an assistant helping the user work with localisation (translation) data in Total War mods.
2687
2688## Understanding Loc Files
2689
2690Loc files contain key-value pairs for in-game text. Each entry has:
2691- A **key** (unique identifier referenced by DB tables)
2692- A **value** (the displayed text in the game)
2693
2694## Viewing Existing Translations
2695
26961. **Open the pack** and **set the game**.
2697
26982. **Decode a loc file**:
2699   Call `decode_packed_file` with the loc file path (e.g. `text/db/my_mod.loc`)
2700   and `source: \"PackFile\"`.
2701
27023. **Get translation overview**:
2703   Call `get_pack_translation` with the pack key and a language code
2704   (e.g. `\"en\"`, `\"fr\"`, `\"de\"`, `\"es\"`, `\"it\"`, `\"zh\"`, `\"ru\"`, etc.).
2705
2706## Creating New Translations
2707
27081. **Create a new loc file**:
2709   Call `new_packed_file` with path `\"text/db/my_mod.loc\"` and
2710   `new_file = {\"Loc\": \"my_mod\"}`.
2711
27122. **Decode it**: `decode_packed_file` to get the empty structure.
2713
27143. **Add entries**: Modify the decoded JSON to add key-value rows.
2715   Each row is typically `[\"key_string\", \"Displayed text in game\"]`.
2716
27174. **Save back**: `save_packed_file_from_view` with the modified data.
2718
2719## Generating Missing Loc Data
2720
2721Call `generate_missing_loc_data` with the pack key to auto-generate
2722loc entries for DB fields that reference loc keys but don't have entries yet.
2723
2724## Finding Loc Keys
2725
2726- Use `go_to_loc` with a loc key to find its source loc file.
2727- Use `get_source_data_from_loc_key` to find where a loc key is referenced.
2728- Use `global_search` with `search_on.loc: true` to search across all loc files.
2729
2730## Tips
2731- Loc keys follow naming conventions like `<table>_<loc_column_name>_<keys_concatenated>`.
2732- Use `search_references` to find all DB columns that reference a specific loc key.
2733- After adding translations, run `diagnostics_check` to verify all references.
2734",
2735        )]
2736    }
2737}