@pbhamri/quartermaster-mcp 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,107 @@
1
- # quartermaster-mcp
1
+ # @pbhamri/quartermaster-mcp
2
2
 
3
- > **MCP server that seeds any repo with the Quartermaster PM kit.**
3
+ > **MCP server that seeds any repo with the Quartermaster PM kit + provides read-only repo access + first-run onboarding + Teams self-agent auto-reply surface.**
4
4
  > No folder dependency. No installer script. Callable from any Copilot / Claude / Cursor session against any cwd.
5
5
 
6
+ **Version:** 0.6.0 | **Transport:** stdio | **Tools:** 19 | **Profiles:** 6 (bundled) + custom
7
+
6
8
  Built in response to PM Architect input (2026-05-29):
7
9
  > *"It would be great if this can be an MCP server that seeds any repo. Then we are not tied to a folder or repo."*
8
10
 
11
+ ---
12
+
9
13
  ## What it does
10
14
 
11
- Exposes 5 tools over MCP stdio:
15
+ Exposes **19 tools** over MCP stdio, organised by workflow:
16
+
17
+ ### Seeding & Scaffolding (v0.1.0+)
12
18
 
13
19
  | Tool | Purpose |
14
20
  |---|---|
15
- | `qm_list_profiles` | List the 6 Purview product profiles bundled in the package |
16
- | `qm_seed_repo` | Seed any repo path with AGENTS.md, copilot-instructions.md, `.quartermaster/profile.json`, `/npf` + `/cxe` prompts, command-center seed. **Idempotent.** Supports `dryRun: true`. |
21
+ | `qm_list_profiles` | List the 6 Purview product profiles (DLM, Billing, CC, Records, eDiscovery, Insider Risk) |
22
+ | `qm_seed_repo` | Seed any repo with AGENTS.md, copilot-instructions.md, `.quartermaster/profile.json`, `/npf` + `/cxe` prompts, command-center seed. **Idempotent.** Supports `dryRun: true`. |
17
23
  | `qm_audit_repo` | Score any repo /6 against the Quartermaster readiness checklist |
18
24
  | `qm_install_prompts` | Install `/npf` + `/cxe` to `~/.github/prompts` |
19
25
  | `qm_apply_profile` | Persist a profile to `~/.copilot/paved-path/profile.json` |
26
+ | `qm_telemetry` | Read recent paved-path telemetry events (type filter, limit) |
27
+ | `qm_emit_pr` | Seed + open a labeled PR via `gh` CLI in one call |
28
+
29
+ ### Read-Only Repo Access (v0.3.0+)
30
+
31
+ | Tool | Purpose |
32
+ |---|---|
33
+ | `repo_overview` | Server metadata: repo_root, framework_count, exclusion rules |
34
+ | `repo_list_dir` | List files/folders under repo_root (denies .git, node_modules, secrets) |
35
+ | `repo_read_file` | Read file content (200 KB cap; secrets/eml/browser sessions blocked) |
36
+ | `repo_search` | Case-insensitive regex search across .md/.js/.ts/.json/.yml/.ps1/.html/.txt |
37
+ | `repo_recent_sessions` | Last N entries from `knowledge/sessions.jsonl` (cross-model memory) |
38
+
39
+ ### Onboarding & Customization (v0.4.0+)
40
+
41
+ | Tool | Purpose |
42
+ |---|---|
43
+ | `qm_welcome` | First-run welcome + 3-step guide for new peers |
44
+ | `qm_personalize` | Interactive Q&A → writes a personalised `connect-db.json`. Never inherits package author's data. |
45
+ | `qm_skills_for_pm` | Curated daily-use catalog: Monday self-assess, midweek PRDs, Friday strategy, sharing |
46
+ | `qm_init_profile` | **Create a custom product profile from scratch** — any product, any org. Generates a reusable `.json` profile with ADO/Kusto/IcM/GitHub connections. |
47
+
48
+ ### Self-Agent / Teams Auto-Reply (v0.5.0+)
20
49
 
21
- ## Install (local, from source)
50
+ | Tool | Purpose |
51
+ |---|---|
52
+ | `qm_autoreply_compose` | Compose a draft Teams auto-reply using second-brain + relationship classifier + failure-mode gates |
53
+ | `qm_autoreply_set_presence` | Set mock presence state (Available/Away/DND/OOO) — P0 skeleton, real Graph swap is P1 |
54
+ | `qm_autoreply_failure_modes` | List named embarrassment-avoidance rules (severity, action, audience) |
55
+ | `qm_autoreply_drafts` | List recent auto-reply drafts under `~/.copilot/autoreply/drafts/` |
56
+
57
+ ---
58
+
59
+ ## Install
60
+
61
+ ### From npm (recommended for peers)
62
+
63
+ ```bash
64
+ npm install -g @pbhamri/quartermaster-mcp
65
+ ```
66
+
67
+ ### From source (contributor)
22
68
 
23
69
  ```powershell
24
- cd C:\Users\pbhamri\source\quartermaster-mcp
70
+ git clone https://github.com/pbhamri_microsoft/Quartermaster.git
71
+ cd Quartermaster/source/quartermaster-mcp
25
72
  npm install
26
73
  ```
27
74
 
28
- Then register in `~\AppData\Roaming\Code\User\mcp.json` (or `.vscode/mcp.json`):
75
+ ### Register with your MCP client
76
+
77
+ **Copilot CLI / VS Code** — add to `~/.copilot/mcp.json` or `.vscode/mcp.json`:
78
+
79
+ ```json
80
+ {
81
+ "servers": {
82
+ "quartermaster": {
83
+ "type": "stdio",
84
+ "command": "npx",
85
+ "args": ["@pbhamri/quartermaster-mcp"]
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
92
+
93
+ ```json
94
+ {
95
+ "mcpServers": {
96
+ "quartermaster": {
97
+ "command": "npx",
98
+ "args": ["@pbhamri/quartermaster-mcp"]
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ **From source (dev):**
29
105
 
30
106
  ```json
31
107
  {
@@ -33,41 +109,109 @@ Then register in `~\AppData\Roaming\Code\User\mcp.json` (or `.vscode/mcp.json`):
33
109
  "quartermaster": {
34
110
  "type": "stdio",
35
111
  "command": "node",
36
- "args": ["C:\\Users\\pbhamri\\source\\quartermaster-mcp\\bin\\server.js"]
112
+ "args": ["<path-to-clone>/source/quartermaster-mcp/bin/server.js"]
37
113
  }
38
114
  }
39
115
  }
40
116
  ```
41
117
 
42
- Reload VS Code. Then in any chat: *"use quartermaster-mcp to seed this repo with purview-dlm"*.
118
+ Reload your MCP client. Then: *"use quartermaster-mcp to seed this repo with purview-dlm"*.
119
+
120
+ ---
43
121
 
44
- ## Usage examples (natural language → MCP)
122
+ ## Usage examples
45
123
 
46
124
  ```text
47
- # List what's available
48
- "List Quartermaster profiles."
125
+ # Onboarding (first time)
126
+ "Run qm_welcome."
127
+ "Personalize my Quartermaster setup."
49
128
 
50
- # Dry-run before writing
51
- "Use quartermaster-mcp to dry-run seed C:\repos\new-repo with purview-cc."
129
+ # Create YOUR product profile (not Purview? no problem)
130
+ "Create a new profile for my product area."
131
+ "I work on Microsoft Intune — set up a Quartermaster profile."
52
132
 
53
- # Real seed
133
+ # Seeding
134
+ "List Quartermaster profiles."
135
+ "Dry-run seed C:\repos\new-repo with purview-cc."
54
136
  "Seed this repo with purview-ediscovery."
137
+ "Seed this repo and open a draft PR."
55
138
 
56
- # Audit any path
139
+ # Auditing
57
140
  "Audit C:\repos\billing-team-repo against Quartermaster readiness."
141
+
142
+ # Repo exploration
143
+ "Show me the repo overview."
144
+ "Search for 'retention policy' in this repo."
145
+ "Read competitive-intel-agent/agent.js."
146
+
147
+ # Self-agent
148
+ "Compose an auto-reply for this message from my manager."
149
+ "Show my failure mode rules."
150
+ "List recent auto-reply drafts."
151
+
152
+ # Telemetry
153
+ "Show my last 20 paved-path events."
58
154
  ```
59
155
 
156
+ ---
157
+
60
158
  ## What gets seeded
61
159
 
62
- In the target repo:
63
- - `AGENTS.md` (only if missing)
64
- - `.github/copilot-instructions.md` (only if missing)
160
+ In the target repo (all idempotent — existing files are **never overwritten**):
161
+
162
+ - `AGENTS.md` repo-specific operating manual
163
+ - `.github/copilot-instructions.md` — AI assistant conventions + CXE pillars
65
164
  - `.quartermaster/profile.json` — full ADO/Kusto/IcM/directives/KPIs snapshot
66
165
  - `.quartermaster/command-center-seed.json` — KPIs + quick-links for dashboards
67
- - `.github/prompts/npf.prompt.md` — NPF self-score
68
- - `.github/prompts/cxe.prompt.md` — Hayete CXE pillar audit
166
+ - `.github/prompts/npf.prompt.md` — NPF self-score (calibrated to IC level)
167
+ - `.github/prompts/cxe.prompt.md` — EVP Security CXE pillar audit
168
+
169
+ ---
170
+
171
+ ## Profiles (6 bundled)
172
+
173
+ | Profile ID | Product Area |
174
+ |---|---|
175
+ | `purview-dlm` | Data Lifecycle Management |
176
+ | `purview-billing` | Purview Billing |
177
+ | `purview-cc` | Communication Compliance |
178
+ | `purview-records` | Records Management |
179
+ | `purview-ediscovery` | eDiscovery |
180
+ | `purview-insider-risk` | Insider Risk Management |
181
+
182
+ Each profile contains: ADO org/project/area-path, Kusto cluster/database/tables, IcM service, default directives, KPI targets, recommended MCP servers, and quick-links.
183
+
184
+ ---
185
+
186
+ ## Architecture
187
+
188
+ ```
189
+ bin/server.js ← Single-file MCP server (stdio transport)
190
+ resources/
191
+ profiles/ ← 6 product profile JSONs + _schema.json
192
+ prompts/ ← /npf + /cxe prompt templates
193
+ connect-db.template.json ← Scaffold for qm_personalize
194
+ mcp-servers/ ← Reference MCP configs for peers
195
+ ```
196
+
197
+ ### Security & Privacy
198
+
199
+ - **Read-only repo access** denies: `.git`, `node_modules`, `.env`, browser sessions, `*.eml`, `*token*`, `*secret*`, `*credentials*`, `*.pem`, `*.key`
200
+ - **No credentials in package** — author's Connect data, ADO tokens, Outlook sessions, and personal NPF history are never bundled
201
+ - **Telemetry is opt-out** — set `QM_METRICS_OPT_OUT=1` to disable. Events use anonymous machine hash (12-char SHA-256 of platform:arch:cpus:totalmem), never username or hostname
202
+ - **Self-agent** skeleton lives at `~/.copilot/self-agent/` (outside this package); tools degrade gracefully if absent
69
203
 
70
- All file writes are idempotent. Existing files are **never overwritten** — they're reported as `skip (exists)`.
204
+ ### Environment Variables
205
+
206
+ | Variable | Default | Purpose |
207
+ |---|---|---|
208
+ | `QM_REPO_ROOT` | `process.cwd()` | Override repo root for read-only access |
209
+ | `QM_CONNECT_DB` | `~/.quartermaster/connect-db.json` | Connect tracker path for `qm_personalize` |
210
+ | `QM_METRICS_FILE` | `~/.copilot/metrics/paved-path-events.jsonl` | Telemetry output path |
211
+ | `QM_METRICS_OPT_OUT` | (unset) | Set to `1` to disable telemetry |
212
+ | `QM_SELF_AGENT_DIR` | `~/.copilot/self-agent` | Self-agent skeleton directory |
213
+
214
+ ---
71
215
 
72
216
  ## Why MCP, not a script
73
217
 
@@ -76,21 +220,43 @@ All file writes are idempotent. Existing files are **never overwritten** — the
76
220
  | Run from one folder | Callable from any cwd in any agent |
77
221
  | Windows PowerShell only | Any client speaking MCP (Copilot CLI, Claude Desktop, Cursor, VS Code) |
78
222
  | Manual re-run per repo | Agent calls it inline mid-conversation |
79
- | Updates require re-share | Single source of truth; bump version, peers pick up via mcp.json |
223
+ | Updates require re-share | `npm update` and peers get the latest |
224
+ | No auto-reply | v0.5.0 adds second-brain-powered Teams draft surface |
225
+
226
+ ---
80
227
 
81
228
  ## Source of truth
82
229
 
83
- Resources are bundled in `resources/` (profiles, prompts, mcp-server registry). To refresh from the live share-kit:
230
+ Profiles are bundled in `resources/profiles/`. The 6 Purview profiles are pre-built. For any other product, use `qm_init_profile` to create your own — it generates a new `.json` in the same directory with your ADO/Kusto/IcM connections.
231
+
232
+ To refresh bundled profiles from an upstream source:
84
233
 
85
234
  ```powershell
86
- Copy-Item C:\Users\pbhamri\share-kit\profiles\*.json resources\profiles\ -Force
87
- Copy-Item C:\Users\pbhamri\.github\prompts\*.prompt.md resources\prompts\ -Force
235
+ # Only maintainers do this; peers just use qm_init_profile
236
+ Copy-Item <source>/profiles/*.json resources/profiles/ -Force
237
+ Copy-Item <source>/prompts/*.prompt.md resources/prompts/ -Force
88
238
  ```
89
239
 
90
240
  Then bump the version in `package.json`.
91
241
 
92
- ## Roadmap (post-v0.1)
242
+ ---
243
+
244
+ ## Version History
245
+
246
+ | Version | Date | What shipped |
247
+ |---|---|---|
248
+ | **0.6.0** | 2026-06-18 | `qm_init_profile` (custom profile creation for any product); stripped all personal data from bundled profiles; `customizableFields` schema |
249
+ | **0.5.0** | 2026-06-10 | Self-agent surface: `qm_autoreply_compose`, `set_presence`, `failure_modes`, `drafts` |
250
+ | **0.4.0** | 2026-06-05 | Onboarding: `qm_welcome`, `qm_personalize`, `qm_skills_for_pm` |
251
+ | **0.3.0** | 2026-06-01 | Read-only repo: `repo_overview`, `repo_list_dir`, `repo_read_file`, `repo_search`, `repo_recent_sessions` |
252
+ | **0.2.0** | 2026-05-30 | `qm_telemetry`, `qm_emit_pr` |
253
+ | **0.1.0** | 2026-05-29 | Core 5: `list_profiles`, `seed_repo`, `audit_repo`, `install_prompts`, `apply_profile` |
254
+
255
+ ---
256
+
257
+ ## Roadmap (post-v0.5)
93
258
 
94
- - `qm_emit_pr`open a PR against a seeded repo with the Quartermaster scaffolding
95
- - `qm_telemetry`emit `paved-path-events.jsonl` entries per seed/audit so adoption is measurable
96
- - Publish to npm as `@pbhamri/quartermaster-mcp` for one-line `npx` install
259
+ - **v0.6**Real Graph `/me/presence` integration (replace mock presence)
260
+ - **v0.7** — `qm_customer_signal` tool (ingest signals directly via MCP, feed to CI agent)
261
+ - **v0.8** Multi-repo seed in one call (batch mode for team onboarding)
262
+ - Publish to Agency Marketplace as a registered MCP server plugin
package/bin/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Quartermaster MCP server — seeds any repo with the Quartermaster PM kit.
3
- // Transport: stdio. Tools: qm_list_profiles, qm_seed_repo, qm_audit_repo, qm_install_prompts, qm_apply_profile.
3
+ // Transport: stdio. Tools: qm_list_profiles, qm_seed_repo, qm_audit_repo, qm_install_prompts, qm_apply_profile, qm_autoreply_* (v0.5.0).
4
4
 
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -107,8 +107,8 @@ function seedRepo({ repoPath, profileId, includeCommandCenter = true, includePro
107
107
  ## Profile-driven defaults
108
108
 
109
109
  - **ADO**: \`${profile.ado.org}/${profile.ado.project}\` · area \`${profile.ado.areaPath}\`
110
- - **Kusto**: \`${profile.kusto.cluster}\` · db \`${profile.kusto.database}\`
111
- - **IcM**: ${profile.icm.serviceName}
110
+ ${profile.kusto ? `- **Kusto**: \`${profile.kusto.cluster}\` · db \`${profile.kusto.database}\`` : "- **Kusto**: not configured"}
111
+ ${profile.icm ? `- **IcM**: ${profile.icm.serviceName}` : "- **IcM**: not configured"}
112
112
 
113
113
  ## Directives (from profile)
114
114
 
@@ -197,7 +197,7 @@ Run \`/cxe\` to audit any PRD/work-item against the 4 pillars. Run \`/npf\` for
197
197
  if (a.target.endsWith("AGENTS.md")) {
198
198
  writeText(a.target, `# AGENTS.md — ${path.basename(repoPath)}\n\n> Seeded by quartermaster-mcp on ${new Date().toISOString().slice(0,10)}.\n> Profile: **${profile.displayName}** (${profile.id})\n\nSee \`.quartermaster/profile.json\` for ADO/Kusto/IcM/directives/KPIs.\n`);
199
199
  } else {
200
- writeText(a.target, `# Copilot Instructions — ${path.basename(repoPath)}\n\nSeeded by quartermaster-mcp · profile **${profile.displayName}**.\n\nCXE pillars (Hayete): CXE-first · Value fast · Confidence · Support when it matters.\nRun /cxe and /npf prompts.\n`);
200
+ writeText(a.target, `# Copilot Instructions — ${path.basename(repoPath)}\n\nSeeded by quartermaster-mcp · profile **${profile.displayName}**.\n\nCXE pillars (EVP Security): CXE-first · Value fast · Confidence · Support when it matters.\nRun /cxe and /npf prompts.\n`);
201
201
  }
202
202
  }
203
203
  }
@@ -400,6 +400,106 @@ function qmPersonalize(args = {}) {
400
400
  };
401
401
  }
402
402
 
403
+ function qmInitProfile(args = {}) {
404
+ // Create a custom product profile from scratch — any PM, any product.
405
+ // Two modes: (1) no answers → returns interview questions; (2) answers → writes profile JSON.
406
+ if (!args.answers) {
407
+ return {
408
+ mode: "interview",
409
+ description: "Create a custom product profile for your area. This generates a .json file you can use with qm_seed_repo and qm_apply_profile.",
410
+ target_dir: path.join(RES, "profiles"),
411
+ questions: [
412
+ { id: "id", q: "Profile ID (lowercase, hyphenated, e.g. 'purview-dlm', 'intune-endpoint', 'teams-calling')", required: true },
413
+ { id: "displayName", q: "Display name (e.g. 'Microsoft Intune Endpoint Management')", required: true },
414
+ { id: "tagline", q: "One-line tagline describing the product area", required: true },
415
+ { id: "ado_org", q: "ADO organization (e.g. 'o365exchange', 'msazure')", required: true },
416
+ { id: "ado_project", q: "ADO project (e.g. 'O365 Core')", required: true },
417
+ { id: "ado_areaPath", q: "ADO area path (e.g. 'O365 Core\\\\Compliance\\\\DLM')", required: true },
418
+ { id: "kusto_cluster",q: "Kusto cluster URL (or 'none')", required: false },
419
+ { id: "kusto_db", q: "Kusto database name (or 'none')", required: false },
420
+ { id: "icm_service", q: "IcM service name (or 'none')", required: false },
421
+ { id: "github_repo", q: "Primary GitHub repo (org/repo format, or 'none')", required: false },
422
+ { id: "directives", q: "Top 3-5 directives (one per line): the outcomes your manager cares about", required: false },
423
+ { id: "kpis", q: "Top 3-5 KPI names (one per line): what you measure weekly", required: false },
424
+ ],
425
+ instructions_for_ai_client: "Ask the user the questions above. When all answers collected, call qm_init_profile again with answers={...} keyed by question id.",
426
+ };
427
+ }
428
+
429
+ // Mode 2: write the profile.
430
+ const a = args.answers;
431
+ if (!a.id || !a.displayName) throw new Error("id and displayName are required");
432
+ const profileId = a.id.toLowerCase().replace(/[^a-z0-9-]/g, "-");
433
+ const profile = {
434
+ id: profileId,
435
+ displayName: a.displayName,
436
+ tagline: a.tagline || "",
437
+ lastUpdated: new Date().toISOString().split("T")[0],
438
+ ado: {
439
+ org: a.ado_org || "{{ADO_ORG}}",
440
+ project: a.ado_project || "{{ADO_PROJECT}}",
441
+ areaPath: a.ado_areaPath || "{{ADO_AREA_PATH}}",
442
+ iterationPath: a.ado_project || "{{ADO_PROJECT}}",
443
+ },
444
+ kusto: a.kusto_cluster && a.kusto_cluster !== "none" ? {
445
+ cluster: a.kusto_cluster,
446
+ database: a.kusto_db || "",
447
+ starterQueries: [],
448
+ } : null,
449
+ icm: a.icm_service && a.icm_service !== "none" ? {
450
+ tenant: "PROD",
451
+ serviceName: a.icm_service,
452
+ teamPublicId: "{{FILL_FROM_ICM_PORTAL}}",
453
+ } : null,
454
+ github: a.github_repo && a.github_repo !== "none" ? {
455
+ primaryRepo: a.github_repo,
456
+ fallbackRepos: [],
457
+ } : null,
458
+ personas: [],
459
+ defaultDirectives: (a.directives || "").split("\n").filter(Boolean).map((t, i) => ({
460
+ id: `D${i + 1}`, title: t.trim(), successSignal: "{{DEFINE}}", cadence: "30 days",
461
+ })),
462
+ defaultKpis: Object.fromEntries(
463
+ (a.kpis || "").split("\n").filter(Boolean).map((k, i) => [
464
+ `kpi${i + 1}`, { label: k.trim(), unit: "{{UNIT}}", target: "{{TARGET}}", owner: "self" }
465
+ ])
466
+ ),
467
+ customizableFields: {
468
+ _description: "Fields below are NOT bundled. Each PM fills them via qm_personalize or by editing .quartermaster/profile.json.",
469
+ fields: [
470
+ { key: "teamContext", type: "object", description: "Engineer count + capacity allocation" },
471
+ { key: "priorities", type: "object", description: "30/60/90 day committed items" },
472
+ { key: "openBugs", type: "object", description: "Bug triage snapshot" },
473
+ { key: "goals", type: "array", description: "CONNECT goals (via qm_personalize)" },
474
+ ],
475
+ },
476
+ recommendedMcp: ["ado"],
477
+ quickLinks: [],
478
+ notes: [`Created via qm_init_profile on ${new Date().toISOString().split("T")[0]}. Fill in {{PLACEHOLDERS}} for your environment.`],
479
+ };
480
+
481
+ // Remove null sections
482
+ if (!profile.kusto) delete profile.kusto;
483
+ if (!profile.icm) delete profile.icm;
484
+ if (!profile.github) delete profile.github;
485
+
486
+ const outPath = path.join(RES, "profiles", `${profileId}.json`);
487
+ if (exists(outPath)) throw new Error(`profile already exists: ${profileId}. Edit it manually or delete first.`);
488
+ writeText(outPath, JSON.stringify(profile, null, 2));
489
+ emit("qm.init_profile", { profileId });
490
+ return {
491
+ mode: "written",
492
+ path: outPath,
493
+ profileId,
494
+ summary: `Created profile '${profileId}' with ${profile.defaultDirectives.length} directives and ${Object.keys(profile.defaultKpis).length} KPIs.`,
495
+ next_steps: [
496
+ `Run \`qm_seed_repo\` with profileId '${profileId}' to scaffold a repo.`,
497
+ "Fill {{PLACEHOLDERS}} in the generated profile for Kusto queries, KPI targets, etc.",
498
+ "Run `qm_personalize` to connect your CONNECT goals.",
499
+ ],
500
+ };
501
+ }
502
+
403
503
  function qmSkillsForPm() {
404
504
  // Curated catalog: 12 MCP tools mapped to PM daily-use cases.
405
505
  // Optimised for product roadmap + strategy work.
@@ -557,6 +657,112 @@ function repoRecentSessions({ limit = 10 } = {}) {
557
657
  return { sessions: lines.map(l => { try { return JSON.parse(l); } catch { return { raw: l }; } }) };
558
658
  }
559
659
 
660
+ // ---------------- v0.5.0 self-agent (Teams auto-reply) surface ----------------
661
+ // The self-agent skeleton lives at ~/.copilot/self-agent/ (outside this package
662
+ // by design — it's a personal workspace artefact). These tools detect it and
663
+ // degrade gracefully if absent.
664
+
665
+ const SELF_AGENT_DIR = process.env.QM_SELF_AGENT_DIR
666
+ || path.join(os.homedir(), ".copilot", "self-agent");
667
+ const AUTOREPLY_DIR = path.join(os.homedir(), ".copilot", "autoreply");
668
+
669
+ function selfAgentInstalled() {
670
+ return exists(path.join(SELF_AGENT_DIR, "reply-composer.js"))
671
+ && exists(path.join(SELF_AGENT_DIR, "brain-query.js"));
672
+ }
673
+
674
+ function requireSelfAgent(mod) {
675
+ if (!selfAgentInstalled()) {
676
+ throw new Error(
677
+ `self-agent skeleton not found at ${SELF_AGENT_DIR}. Install it (see `
678
+ + `https://www.npmjs.com/package/@pbhamri/quartermaster-mcp) or set QM_SELF_AGENT_DIR env var.`
679
+ );
680
+ }
681
+ return require(path.join(SELF_AGENT_DIR, mod));
682
+ }
683
+
684
+ function qmAutoreplyCompose({ message, senderName = "Test Sender", senderEmail = "test@example.com", tier } = {}) {
685
+ if (!message || typeof message !== "string") throw new Error("message (string) is required");
686
+ const { queryBrain } = requireSelfAgent("brain-query.js");
687
+ const { compose } = requireSelfAgent("reply-composer.js");
688
+ const { loadProfile } = requireSelfAgent("behavior-profile.js");
689
+ const { classify } = requireSelfAgent("relationship-classifier.js");
690
+ const profile = loadProfile();
691
+ const sender = { name: senderName, email: senderEmail };
692
+ const rel = tier || classify(sender);
693
+ const brain = queryBrain(message);
694
+ const out = compose(message, brain, { sender, relationship: rel, profile });
695
+ return {
696
+ inbound: { message, sender, relationship: rel },
697
+ composed: {
698
+ tier: out.tier,
699
+ action: out.action,
700
+ confidence_pct: out.confidencePct,
701
+ voice_match: out.voiceMatch,
702
+ stances_applied: out.appliedStances,
703
+ failure_mode_violations: out.failureModeViolations || [],
704
+ failure_mode_blocked_by: out.failureModeBlockedBy || null,
705
+ leverage_hint: out.leverageHint || null,
706
+ stale_notice: out.staleNotice || null,
707
+ body: out.body,
708
+ },
709
+ brain_top_source: brain.topSource || null,
710
+ };
711
+ }
712
+
713
+ function qmAutoreplySetPresence({ state } = {}) {
714
+ if (!state) throw new Error("state is required (Available|Away|Offline|BeRightBack|OutOfOffice|DoNotDisturb)");
715
+ const { setMockState, AWAY_STATES } = requireSelfAgent("presence-watcher.js");
716
+ setMockState(state);
717
+ const away = AWAY_STATES instanceof Set ? AWAY_STATES.has(state) : (AWAY_STATES || []).includes(state);
718
+ return { state, away, note: "mock presence (P0 skeleton; real Graph swap is P1)" };
719
+ }
720
+
721
+ function qmAutoreplyFailureModes() {
722
+ const { loadRules } = requireSelfAgent("failure-modes.js");
723
+ const rules = loadRules();
724
+ return {
725
+ count: rules.length,
726
+ rules: rules.map(r => ({
727
+ id: r.id,
728
+ severity: r.severity || "auto-fix",
729
+ action: r.action || null,
730
+ audience: r.audience || "any",
731
+ description: r.description || "",
732
+ })),
733
+ note: `Edit ${path.join(SELF_AGENT_DIR, "failure-modes.json")} to add named embarrassment scenarios.`,
734
+ };
735
+ }
736
+
737
+ function qmAutoreplyDrafts({ limit = 10 } = {}) {
738
+ const draftsDir = path.join(AUTOREPLY_DIR, "drafts");
739
+ if (!exists(draftsDir)) return { drafts: [], note: `no drafts dir at ${draftsDir}` };
740
+ const files = fs.readdirSync(draftsDir)
741
+ .filter(f => f.endsWith(".txt"))
742
+ .map(f => ({ file: f, mtime: fs.statSync(path.join(draftsDir, f)).mtime.toISOString() }))
743
+ .sort((a, b) => b.mtime.localeCompare(a.mtime))
744
+ .slice(0, limit);
745
+ const drafts = files.map(({ file, mtime }) => {
746
+ const full = path.join(draftsDir, file);
747
+ const head = fs.readFileSync(full, "utf8").split("\n").slice(0, 20);
748
+ const grab = (label) => {
749
+ const line = head.find(l => l.startsWith(`# ${label}:`));
750
+ return line ? line.slice(label.length + 3).trim() : null;
751
+ };
752
+ return {
753
+ file,
754
+ mtime,
755
+ to: grab("To"),
756
+ tier: grab("Tier"),
757
+ relationship: grab("Relationship"),
758
+ violations: grab("Failure-mode violations"),
759
+ blocked_by: grab("Failure-mode blocked-by"),
760
+ inbound: grab("Inbound text"),
761
+ };
762
+ });
763
+ return { count: drafts.length, drafts };
764
+ }
765
+
560
766
 
561
767
  // ---------------- MCP wiring ----------------
562
768
  const server = new Server(
@@ -597,6 +803,8 @@ const TOOLS = [
597
803
  inputSchema: { type: "object", properties: { answers: { type: "object", description: "Key-value of question ids to user answers. Returned by interview mode." } } } },
598
804
  { name: "qm_skills_for_pm", description: "Curated catalog of the Quartermaster tools mapped to PM daily-use workflows (Monday self-assess, midweek PRDs + roadmap, Friday strategy prep, onboarding + sharing).",
599
805
  inputSchema: { type: "object", properties: {} } },
806
+ { name: "qm_init_profile", description: "Create a custom product profile from scratch for ANY product area (not just Purview). Returns an interview flow, then writes a reusable .json profile. Use this when no bundled profile matches your product.",
807
+ inputSchema: { type: "object", properties: { answers: { type: "object", description: "Key-value of question ids to user answers. Returned by interview mode." } } } },
600
808
  // ---- v0.3.0 read-only repo surface ----
601
809
  { name: "repo_overview", description: "Returns server metadata: repo_root, framework_count (if a competitive-intel-agent/frameworks dir exists), exclusion rules. No args.",
602
810
  inputSchema: { type: "object", properties: {} } },
@@ -608,6 +816,23 @@ const TOOLS = [
608
816
  inputSchema: { type: "object", required: ["query"], properties: { query: { type: "string" }, max: { type: "integer" } } } },
609
817
  { name: "repo_recent_sessions", description: "Returns the last N entries from knowledge/sessions.jsonl (cross-model session memory) if present. Args: limit (default 10).",
610
818
  inputSchema: { type: "object", properties: { limit: { type: "integer" } } } },
819
+ // ---- v0.5.0 self-agent (Teams auto-reply) surface ----
820
+ // Requires the self-agent skeleton at ~/.copilot/self-agent/ (or QM_SELF_AGENT_DIR override).
821
+ { name: "qm_autoreply_compose", description: "Compose a draft Teams auto-reply for an inbound message using the user's second brain. Returns the body + confidence% + tier + named-failure-mode violations. No send.",
822
+ inputSchema: { type: "object", required: ["message"], properties: {
823
+ message: { type: "string", description: "Inbound message text" },
824
+ senderName: { type: "string", description: "Sender display name (optional; affects relationship classifier)" },
825
+ senderEmail: { type: "string", description: "Sender email (optional)" },
826
+ tier: { type: "string", description: "Override relationship: manager|skip|peer|report|cross-org|unknown" },
827
+ } } },
828
+ { name: "qm_autoreply_set_presence", description: "Set the mock presence state for the self-agent (P0 skeleton; real Graph /me/presence is the P1 swap).",
829
+ inputSchema: { type: "object", required: ["state"], properties: {
830
+ state: { type: "string", enum: ["Available","Away","Offline","BeRightBack","OutOfOffice","DoNotDisturb"] },
831
+ } } },
832
+ { name: "qm_autoreply_failure_modes", description: "List the named embarrassment-avoidance rules currently loaded by the self-agent (id, severity, action, audience, description).",
833
+ inputSchema: { type: "object", properties: {} } },
834
+ { name: "qm_autoreply_drafts", description: "List the most recent auto-reply drafts written under ~/.copilot/autoreply/drafts (with tier, violations, inbound text).",
835
+ inputSchema: { type: "object", properties: { limit: { type: "integer", default: 10 } } } },
611
836
  ];
612
837
 
613
838
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
@@ -627,11 +852,16 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
627
852
  case "qm_welcome": result = qmWelcome(); break;
628
853
  case "qm_personalize": result = qmPersonalize(args); break;
629
854
  case "qm_skills_for_pm": result = qmSkillsForPm(); break;
855
+ case "qm_init_profile": result = qmInitProfile(args); break;
630
856
  case "repo_overview": result = repoOverview(); break;
631
857
  case "repo_list_dir": result = repoListDir(args); break;
632
858
  case "repo_read_file": result = repoReadFile(args); break;
633
859
  case "repo_search": result = repoSearch(args); break;
634
860
  case "repo_recent_sessions": result = repoRecentSessions(args); break;
861
+ case "qm_autoreply_compose": result = await timed("qm.autoreply_compose", {}, async () => qmAutoreplyCompose(args)); break;
862
+ case "qm_autoreply_set_presence": result = await timed("qm.autoreply_set_presence", { state: args.state }, async () => qmAutoreplySetPresence(args)); break;
863
+ case "qm_autoreply_failure_modes": result = qmAutoreplyFailureModes(); break;
864
+ case "qm_autoreply_drafts": result = qmAutoreplyDrafts(args); break;
635
865
  default: throw new Error(`unknown tool: ${name}`);
636
866
  }
637
867
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pbhamri/quartermaster-mcp",
3
- "version": "0.4.0",
4
- "description": "MCP server that seeds any repo with the Quartermaster PM kit + provides read-only repo access + first-run onboarding (qm_welcome, qm_personalize, qm_skills_for_pm). Personalised per peer; never inherits the package author's Connect/credentials/personal data. One-command share with any MS PM peer: npx -y @pbhamri/quartermaster-mcp",
3
+ "version": "0.6.0",
4
+ "description": "MCP server that seeds any repo with a customizable PM kit. Any PM, any product connect YOUR GitHub, ADO, Kusto, IcM. 19 tools: scaffolding, repo-read, onboarding, custom profile creation, self-agent auto-reply. Never ships personal data profiles are templates you fill in.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "quartermaster-mcp": "bin/server.js"
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "id": "purview-dlm",
3
3
  "displayName": "Purview Data Lifecycle Management (DLM)",
4
- "tagline": "Retention labels, retention policies, records, disposition across SharePoint, OneDrive, Exchange, Teams.",
4
+ "tagline": "Retention labels, retention policies, records, disposition -- across SharePoint, OneDrive, Exchange, Teams, Copilot.",
5
+ "lastUpdated": "2026-06-18",
5
6
  "ado": {
6
7
  "org": "o365exchange",
7
8
  "project": "O365 Core",
@@ -12,49 +13,68 @@
12
13
  "cluster": "https://cxedataplatformcluster.westus2.kusto.windows.net/",
13
14
  "database": "PurviewTelemetry",
14
15
  "starterQueries": [
15
- { "name": "DLM 28d retention-label apply rate", "kql": "RetentionLabelEvents | where Timestamp > ago(28d) | summarize Applied = count() by bin(Timestamp, 1d)" },
16
- { "name": "Disposition review queue depth", "kql": "DispositionReviewEvents | where State == 'PendingReview' | summarize Pending = count() by Workload" },
17
- { "name": "E5 vs E3 active-tenant DLM feature usage","kql": "DlmFeatureUsage | where Timestamp > ago(30d) | summarize ActiveTenants = dcount(TenantId) by Sku, FeatureName" }
16
+ { "name": "DLM 28d retention-label apply rate", "kql": "RetentionLabelEvents | where Timestamp > ago(28d) | summarize Applied = count() by bin(Timestamp, 1d)" },
17
+ { "name": "Disposition review queue depth", "kql": "DispositionReviewEvents | where State == 'PendingReview' | summarize Pending = count() by Workload" },
18
+ { "name": "E5 vs E3 active-tenant DLM feature usage", "kql": "DlmFeatureUsage | where Timestamp > ago(30d) | summarize ActiveTenants = dcount(TenantId) by Sku, FeatureName" },
19
+ { "name": "Policy sync reliability", "kql": "PolicySyncEvents | where Timestamp > ago(7d) | summarize SuccessRate = round(100.0 * countif(Status == 'Success') / count(), 2) by bin(Timestamp, 1d)" },
20
+ { "name": "DLM ICM volume trend", "kql": "IcmIncidents | where ServiceName has 'Data Lifecycle' and CreateDate > ago(90d) | summarize ICMs = count() by bin(CreateDate, 7d)" }
18
21
  ]
19
22
  },
20
23
  "icm": {
21
24
  "tenant": "PROD",
22
25
  "serviceName": "Microsoft Purview / Data Lifecycle Management",
23
- "teamPublicId": "TBD-fill-from-IcM-portal",
24
- "routingId": "DLM-PM-Onсall"
26
+ "teamPublicId": "{{FILL_FROM_ICM_PORTAL}}",
27
+ "routingId": "DLM-PM-Oncall"
25
28
  },
26
29
  "github": {
27
30
  "primaryRepo": "microsoft/m365-purview-dlm",
28
31
  "fallbackRepos": ["microsoft/m365-purview"]
29
32
  },
30
33
  "personas": [
31
- { "role": "Engineering Manager", "exampleName": "TBD" },
32
- { "role": "Design Partner Enterprise", "exampleName": "TBD" },
33
- { "role": "Compliance Marketing PMM", "exampleName": "TBD" },
34
- { "role": "Copilot Compliance PM", "exampleName": "TBD" }
34
+ { "role": "Engineering Manager" },
35
+ { "role": "Design Partner -- Enterprise" },
36
+ { "role": "Compliance Marketing PMM" },
37
+ { "role": "Copilot Compliance PM" },
38
+ { "role": "FastTrack Architect" }
35
39
  ],
36
40
  "defaultDirectives": [
37
- { "id": "D1", "title": "Ramp-up: convert insights into 2-3 backlog wins with named customer evidence", "successSignal": "3 ADO items filed with customer quote + tenant ID", "cadence": "30 days" },
38
- { "id": "D2", "title": "E3 E5 adoption: 25% lift target", "successSignal": "30-day baseline read, then weekly trend", "cadence": "Weekly Mon" },
39
- { "id": "D3", "title": "AI in DLM: ship 1-2 surfaces with value metrics", "successSignal": "Minutes saved + queue collapse + false-positive reduction", "cadence": "Monthly readout" },
40
- { "id": "D4", "title": "Safe AI battlecard: co-own with Copilot + Compliance marketing", "successSignal": "Battlecard published + sales-enabled", "cadence": "60 days" },
41
- { "id": "D5", "title": "Cross-functional 1:1s evolve from info-gathering to joint problem-solving", "successSignal": "Each 1:1 brings a specific hypothesis", "cadence": "Weekly" }
41
+ { "id": "D1", "title": "Ship retention insights for AI app interactions (Copilot compliance)", "successSignal": "Committed customers in preview; CXE pillar 2 (TTV) met", "cadence": "30 days" },
42
+ { "id": "D2", "title": "Reduce DLM support volume via self-serve diagnostics", "successSignal": "10% IPD reduction in Sev-C ICM volume vs baseline", "cadence": "60 days" },
43
+ { "id": "D3", "title": "Priority Cleanup + Hard Delete to GA (ODSP)", "successSignal": "GA milestone hit; 10+ customers in first 30d", "cadence": "30 days" },
44
+ { "id": "D4", "title": "E3 -> E5 attach via DLM-originated trial starts", "successSignal": "+25% WoW trial-start -> attach-rate -> paid-conversion", "cadence": "Weekly" },
45
+ { "id": "D5", "title": "Shared leverage: PM toolkit adopted by 3+ peers", "successSignal": "3 distinct users confirmed via paved-path telemetry", "cadence": "90 days" }
42
46
  ],
43
47
  "defaultKpis": {
44
- "e5BaselineLift": { "label": "E3 E5 lift", "unit": "% MoM", "target": "+25% in 90d", "owner": "self" },
45
- "aiSurfacesShipped": { "label": "AI surfaces shipped", "unit": "/2 chosen", "target": "2 by 2026-06-13", "owner": "self" },
46
- "backlogWinsFiled": { "label": "ADO backlog wins from ramp-up", "unit": "/3", "target": "3 by 2026-06-10", "owner": "self" },
47
- "dispositionTimeBefore":{ "label": "Disposition time baseline", "unit": "minutes", "target": "capture by 2026-06-05", "owner": "self" },
48
- "dispositionTimeAfter": { "label": "Disposition time — with AI", "unit": "minutes", "target": "-50% vs baseline", "owner": "self" }
48
+ "copilotRetention": { "label": "Copilot retention -- preview customers", "unit": "customers", "target": "{{SET_TARGET}}", "owner": "self" },
49
+ "policySyncReliability":{ "label": "Policy Sync success rate", "unit": "%", "target": "99.9%", "owner": "engineering" },
50
+ "icmReduction": { "label": "DLM ICM volume reduction", "unit": "% vs baseline","target": "-10%", "owner": "self + eng" },
51
+ "e5TrialAttach": { "label": "E3 -> E5 trial-start WoW lift", "unit": "% WoW", "target": "+25%", "owner": "self" },
52
+ "dispositionTTM": { "label": "Disposition end-to-end time", "unit": "days", "target": "<=30d", "owner": "engineering" },
53
+ "peerAdoption": { "label": "PM toolkit peer adoption", "unit": "users", "target": "3+", "owner": "self" }
54
+ },
55
+ "customizableFields": {
56
+ "_description": "These fields are NOT bundled. Each PM fills them in via qm_personalize or by editing their local .quartermaster/profile.json after seeding.",
57
+ "fields": [
58
+ { "key": "teamContext.engineerCount", "type": "integer", "description": "Number of engineers on your DLM sub-team" },
59
+ { "key": "teamContext.capacityAllocation","type": "object", "description": "Capacity split across workstreams (keys are workstream names, values are fractions summing to 1.0)" },
60
+ { "key": "priorities.thirtyDayItems", "type": "string[]","description": "Your 30-day committed items (from manager connect or sprint plan)" },
61
+ { "key": "priorities.sixtyDayItems", "type": "string[]","description": "Your 60-day committed items" },
62
+ { "key": "priorities.ninetyDayItems", "type": "string[]","description": "Your 90-day stretch items" },
63
+ { "key": "openBugs", "type": "object", "description": "Your triaged bug snapshot (totalOpen, highlighted array)" },
64
+ { "key": "goals", "type": "array", "description": "Your CONNECT goals -- generated via qm_personalize" },
65
+ { "key": "quickLinks", "type": "array", "description": "Your team's SharePoint, OneNote, dashboard links" }
66
+ ]
49
67
  },
50
68
  "recommendedMcp": ["kusto", "icm", "workiq", "ado", "playwright"],
51
69
  "quickLinks": [
52
- { "label": "DLM PM workspace", "url": "https://aka.ms/purview-dlm-pm" },
53
- { "label": "DLM ADO board", "url": "https://dev.azure.com/o365exchange/O365%20Core/_boards/board/t/DLM" },
54
- { "label": "Retention docs (M365)", "url": "https://learn.microsoft.com/purview/retention" }
70
+ { "label": "DLM ADO board", "url": "https://dev.azure.com/o365exchange/O365%20Core/_boards/board/t/DLM" },
71
+ { "label": "Retention docs (M365)", "url": "https://learn.microsoft.com/purview/retention" },
72
+ { "label": "DLM feature flags", "url": "https://aka.ms/purview-dlm-flags" }
55
73
  ],
56
74
  "notes": [
57
- "Fill teamPublicId from icmportal Service Tree 'Microsoft Purview / Data Lifecycle Management' 'Team' tab.",
58
- "Disposition KPI baseline must be captured BEFORE AI-in-DLM ships otherwise no proof of value metric."
75
+ "Fill teamPublicId from icmportal -> Service Tree -> 'Microsoft Purview / Data Lifecycle Management' -> 'Team' tab.",
76
+ "Run qm_personalize after seeding to fill in your goals, priorities, and team context.",
77
+ "KPI targets marked {{SET_TARGET}} must be filled by the PM during personalization.",
78
+ "Kusto queries are product-wide and shared across DLM PMs -- customize for your sub-area in local profile."
59
79
  ]
60
80
  }