Skip to main content

teamcraft/mcp/tools/
prompts.rs

1//! The MCP prompts (`prompts/list`, `prompts/get`).
2
3use super::*;
4
5pub(super) struct Prompt {
6    pub(super) name: &'static str,
7    title: &'static str,
8    description: &'static str,
9    /// (name, description, required)
10    arguments: &'static [(&'static str, &'static str, bool)],
11}
12
13pub(super) const PROMPTS: &[Prompt] = &[
14    Prompt {
15        name: "release_notes",
16        title: "Release notes",
17        description: "Release notes for a revision: what changed compared with the previous one.",
18        arguments: &[
19            ("item", "Item number, e.g. ASM-000003", true),
20            ("revision", "Revision code (default: latest)", false),
21        ],
22    },
23    Prompt {
24        name: "review_change",
25        title: "Review a change order",
26        description: "Prepares the review of a change order: scope, risks, open questions.",
27        arguments: &[("change", "Change number, e.g. ECO-000004", true)],
28    },
29    Prompt {
30        name: "release_check",
31        title: "Release check",
32        description: "Checks whether a revision and its structure are ready to be released.",
33        arguments: &[
34            ("item", "Item number", true),
35            ("revision", "Revision code (default: latest)", false),
36        ],
37    },
38    Prompt {
39        name: "worklist_triage",
40        title: "Worklist triage",
41        description: "Orders what is waiting for me into a short to-do list.",
42        arguments: &[],
43    },
44    Prompt {
45        name: "triage_requests",
46        title: "Prepare request triage",
47        description:
48            "Summarises the open change requests assigned to me (and new unassigned ones) so I \
49            can triage them; the decisions stay mine.",
50        arguments: &[],
51    },
52    Prompt {
53        name: "simulation_status",
54        title: "Simulation status",
55        description:
56            "Whether the simulations of a part or assembly – or one simulation – still match \
57            their bases, and how the results look.",
58        arguments: &[
59            (
60                "item",
61                "Part, assembly or simulation number, e.g. SIM-000003",
62                true,
63            ),
64            ("revision", "Revision code (default: latest)", false),
65        ],
66    },
67];
68
69/// `prompts/list`.
70pub fn prompts() -> Vec<Value> {
71    PROMPTS
72        .iter()
73        .map(|p| {
74            json!({
75                "name": p.name,
76                "title": p.title,
77                "description": p.description,
78                "arguments": p.arguments.iter().map(|(name, description, required)| json!({
79                    "name": name, "description": description, "required": required,
80                })).collect::<Vec<_>>(),
81            })
82        })
83        .collect()
84}
85
86/// `prompts/get`: the prompt as one user message.
87pub fn prompt(name: &str, args: &Value) -> Result<Value, String> {
88    let p = PROMPTS
89        .iter()
90        .find(|p| p.name == name)
91        .ok_or_else(|| format!("unknown prompt `{name}`"))?;
92    let arg = |key: &str| -> Option<String> {
93        args.get(key)
94            .and_then(Value::as_str)
95            .map(str::trim)
96            .filter(|s| !s.is_empty())
97            .map(str::to_string)
98    };
99    for (key, _, required) in p.arguments {
100        if *required && arg(key).is_none() {
101            return Err(format!("prompt `{name}` needs the argument `{key}`"));
102        }
103    }
104    let revision = arg("revision").unwrap_or_else(|| "latest".into());
105    let text = match name {
106        "release_notes" => {
107            let item = arg("item").unwrap_or_default();
108            format!(
109                "Write release notes for {item} revision {revision} in TeamCraft.\n\
110                 1. Call compare_revisions with item={item} and to={revision} (it compares with the revision before).\n\
111                 2. Call get_revision for {item}/{revision} for the description and the change orders it belongs to; \
112                 if there is one, call get_change for its title and reason.\n\
113                 Produce: a one-line summary; then short sections Attributes, Files (added, removed, new content) and BOM \
114                 (added, removed, quantity changes), leaving out empty sections; then Open points (e.g. unreleased children, \
115                 missing mass). Quote item numbers and revisions exactly. Do not invent reasons the data does not give."
116            )
117        }
118        "review_change" => {
119            let change = arg("change").unwrap_or_default();
120            format!(
121                "Help me review change order {change} in TeamCraft.\n\
122                 1. Call get_change with change={change}.\n\
123                 2. For each revision in it, call compare_revisions (item, to=its revision) to see what it changes; \
124                 for assemblies also call bom_health with rule=released.\n\
125                 3. Call suggest_reviewers with change={change} to see who else knows the affected items.\n\
126                 Produce a concise review for an engineer: Scope (what is released, in one line each); Risks (readiness \
127                 blockers and warnings, dependents from the impact analysis that are not in the change, check-outs, \
128                 missing files or mass); Questions for the author; people who should also look at it (only from \
129                 suggest_reviewers, with its reason; it never assigns them); a suggestion whether it looks ready. The decision \
130                 (approve or reject) is mine – you cannot and should not make it."
131            )
132        }
133        "release_check" => {
134            let item = arg("item").unwrap_or_default();
135            format!(
136                "Check whether {item} revision {revision} is ready to be released.\n\
137                 1. Call get_revision for {item}/{revision}.\n\
138                 2. Call bom_health with item={item}, revision={revision}, rule=released.\n\
139                 3. Call find_duplicate_files with item={item}, revision={revision}.\n\
140                 4. If it is a new part (revision A), call find_similar_parts with item={item}, revision={revision} for \
141                 existing parts of nearly the same shape (score 90 or more).\n\
142                 Produce a checklist: Blocking (must be fixed or released together: unreleased or unresolved BOM lines and \
143                 references, check-outs) and Warnings (missing mass or files, obsolete parts, duplicate files, near-duplicate \
144                 existing parts, open \
145                 checks). End with the items a change order would need. Offer to draft it with create_change_draft, but \
146                 only create it if I say so."
147            )
148        }
149        "worklist_triage" => "Triage my TeamCraft worklist.\n\
150             Call my_worklist. Produce a short ordered to-do list: reviews waiting for my decision first (oldest \
151             first), then open impacts to check, then check-outs that have been open long (remind me to check in or \
152             cancel), then my own change orders that are drafts or were rejected. One line per entry with the item or \
153             change number and the next action. Say plainly if there is nothing to do."
154            .to_string(),
155        "triage_requests" => "Prepare the triage of my TeamCraft change requests.\n\
156             1. Call list_change_requests with scope=assigned and status=open.\n\
157             2. Call list_change_requests with status=new for requests nobody has looked at yet.\n\
158             3. For each request found (at most 10, highest priority and oldest due date first), call \
159             get_change_request; for an affected item that looks central, call get_item for its state, open \
160             change orders and simulations.\n\
161             Produce one short block per request: number and title; the problem in one sentence; affected items \
162             with their latest state; what the discussion already settled and what is open; duplicates or \
163             related requests; a suggested next step (ask the requester, check a simulation, candidate for \
164             accepting, deferring or rejecting – with the reason). You cannot triage, accept, defer or reject: \
165             those decisions are mine and are made in TeamCraft. Say plainly if there is nothing to triage."
166            .to_string(),
167        "simulation_status" => {
168            let item = arg("item").unwrap_or_default();
169            format!(
170                "Report the simulation status of {item} revision {revision} in TeamCraft.\n\
171                 1. Call get_item with item={item}. If it is a simulation, call get_simulation with item={item} \
172                 and revision={revision}, and compare_simulation_results with item={item} and to={revision} when \
173                 there is an earlier revision.\n\
174                 2. Otherwise call list_simulations with item={item}, then get_simulation for each simulation \
175                 that is out of date or has failed results.\n\
176                 Produce: a table of the simulations (number, revision, state, up to date or the out-of-date \
177                 reason, results passed/failed); then the failed values with value, limit and load case; then \
178                 what changed in the results between the revisions, if compared; then a recommendation which \
179                 simulations need to be re-run or re-checked. Do not invent values the data does not give."
180            )
181        }
182        _ => unreachable!("prompt names are checked above"),
183    };
184    Ok(json!({
185        "description": p.description,
186        "messages": [{ "role": "user", "content": { "type": "text", "text": text } }],
187    }))
188}