@tideorg/mcp 1.4.1 → 1.9.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/GAP_REGISTER.md +19 -9
  3. package/PRIVACY.md +41 -0
  4. package/README.md +204 -120
  5. package/adapters/AGENTS.md +3 -2
  6. package/adapters/CLAUDE.md +1 -1
  7. package/adapters/replit.md +0 -0
  8. package/canon/anti-patterns.md +55 -62
  9. package/canon/concepts.md +46 -34
  10. package/canon/custom-contracts.md +12 -8
  11. package/canon/feature-mapping.md +27 -75
  12. package/canon/framework-matrix.md +69 -22
  13. package/canon/hosting-options.md +111 -0
  14. package/canon/iga-change-requests-api.md +211 -0
  15. package/canon/invariants.md +33 -35
  16. package/canon/redirect-handler.md +0 -0
  17. package/canon/security-gap-mapping.md +506 -0
  18. package/canon/security-runtime-probes.md +177 -0
  19. package/canon/tidecloak-bootstrap.md +84 -15
  20. package/canon/tidecloak-endpoints.md +60 -98
  21. package/canon/troubleshooting.md +201 -53
  22. package/canon/version-policy.md +8 -12
  23. package/mcp-server/dist/http.d.ts +2 -0
  24. package/mcp-server/dist/http.js +46 -0
  25. package/mcp-server/dist/index.d.ts +0 -0
  26. package/mcp-server/dist/index.js +2 -601
  27. package/mcp-server/dist/server.d.ts +2 -0
  28. package/mcp-server/dist/server.js +617 -0
  29. package/package.json +48 -45
  30. package/playbooks/add-auth-nextjs-existing.md +33 -17
  31. package/playbooks/add-auth-nextjs-fresh.md +1 -1
  32. package/playbooks/add-rbac-nextjs.md +7 -2
  33. package/playbooks/bootstrap-realm-from-template.md +33 -18
  34. package/playbooks/configure-e2ee-roles-and-policies.md +0 -0
  35. package/playbooks/deploy-tidecloak-docker.md +31 -28
  36. package/playbooks/diagnose-broken-login.md +0 -0
  37. package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
  38. package/playbooks/initialize-admin-and-link-account.md +22 -19
  39. package/playbooks/migrate-from-existing-auth.md +0 -0
  40. package/playbooks/protect-api-nextjs.md +40 -1
  41. package/playbooks/protect-aspnet-core-asgard.md +313 -0
  42. package/playbooks/protect-routes-nextjs.md +24 -10
  43. package/playbooks/provision-tidecloak-skycloak.md +213 -0
  44. package/playbooks/setup-forseti-e2ee.md +32 -50
  45. package/playbooks/setup-iga-admin-panel.md +113 -171
  46. package/playbooks/start-tidecloak-dev.md +0 -0
  47. package/playbooks/verify-jwt-server-side.md +20 -1
  48. package/prompts/add-admin-approval-flow.md +0 -0
  49. package/prompts/build-private-customer-portal.md +1 -1
  50. package/prompts/migrate-generic-auth-to-tide.md +0 -0
  51. package/prompts/secure-existing-app.md +0 -0
  52. package/prompts/security-gap-analysis.md +55 -0
  53. package/reference-apps/INDEX.md +0 -0
  54. package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
  55. package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
  56. package/reference-apps/encrypted-communication/manifest.yaml +0 -0
  57. package/reference-apps/encrypted-communication/role-policy-matrix.md +0 -0
  58. package/reference-apps/encrypted-communication/scenario.md +2 -2
  59. package/reference-apps/git-pr-signing-service/anti-patterns.md +0 -0
  60. package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
  61. package/reference-apps/git-pr-signing-service/manifest.yaml +0 -0
  62. package/reference-apps/git-pr-signing-service/role-policy-matrix.md +0 -0
  63. package/reference-apps/git-pr-signing-service/scenario.md +0 -0
  64. package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
  65. package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
  66. package/reference-apps/iga-admin-governance/manifest.yaml +0 -0
  67. package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
  68. package/reference-apps/iga-admin-governance/scenario.md +15 -13
  69. package/reference-apps/organisation-password-manager/anti-patterns.md +0 -0
  70. package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
  71. package/reference-apps/organisation-password-manager/manifest.yaml +0 -0
  72. package/reference-apps/organisation-password-manager/role-policy-matrix.md +0 -0
  73. package/reference-apps/organisation-password-manager/scenario.md +0 -0
  74. package/reference-apps/policy-governed-signing/anti-patterns.md +0 -0
  75. package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
  76. package/reference-apps/policy-governed-signing/manifest.yaml +0 -0
  77. package/reference-apps/policy-governed-signing/role-policy-matrix.md +0 -0
  78. package/reference-apps/policy-governed-signing/scenario.md +0 -0
  79. package/skills/tide-diagnostics/SKILL.md +1 -1
  80. package/skills/tide-integration/SKILL.md +9 -18
  81. package/skills/tide-learning-capture/SKILL.md +0 -0
  82. package/skills/tide-mcp-qa/SKILL.md +139 -0
  83. package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
  84. package/skills/tide-reviewer/SKILL.md +1 -1
  85. package/skills/tide-route-and-api-protection/SKILL.md +0 -0
  86. package/skills/tide-scenario-resolver/SKILL.md +0 -0
  87. package/skills/tide-security-analyst/SKILL.md +182 -0
  88. package/skills/tide-setup/SKILL.md +1 -1
  89. package/skills/tide-solutions-architect/SKILL.md +0 -0
  90. package/canon/delegation.md +0 -195
  91. package/playbooks/setup-server-delegation.md +0 -142
@@ -0,0 +1,617 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { readFileSync, readdirSync, existsSync } from "fs";
4
+ import { join, resolve } from "path";
5
+ // Resolve pack root.
6
+ // - TIDE_PACK_ROOT env var takes priority (custom deployments)
7
+ // - Default: two levels up from dist/ → repo/package root
8
+ const PACK_ROOT = process.env.TIDE_PACK_ROOT
9
+ ? resolve(process.env.TIDE_PACK_ROOT)
10
+ : resolve(import.meta.dirname, "..", "..");
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+ function listMarkdownFiles(dir) {
15
+ const full = join(PACK_ROOT, dir);
16
+ if (!existsSync(full))
17
+ return [];
18
+ return readdirSync(full)
19
+ .filter((f) => f.endsWith(".md"))
20
+ .map((f) => f.replace(/\.md$/, ""));
21
+ }
22
+ function listDirectories(dir) {
23
+ const full = join(PACK_ROOT, dir);
24
+ if (!existsSync(full))
25
+ return [];
26
+ return readdirSync(full, { withFileTypes: true })
27
+ .filter((d) => d.isDirectory())
28
+ .map((d) => d.name);
29
+ }
30
+ function readPackFile(dir, name) {
31
+ const file = name.endsWith(".md") ? name : `${name}.md`;
32
+ const full = join(PACK_ROOT, dir, file);
33
+ if (!existsSync(full))
34
+ return null;
35
+ return readFileSync(full, "utf-8");
36
+ }
37
+ function readSkill(name) {
38
+ const full = join(PACK_ROOT, "skills", name, "SKILL.md");
39
+ if (!existsSync(full))
40
+ return null;
41
+ return readFileSync(full, "utf-8");
42
+ }
43
+ function scenarioExists(scenario) {
44
+ return existsSync(join(PACK_ROOT, "reference-apps", scenario));
45
+ }
46
+ function readScenarioFile(scenario, fileName) {
47
+ const full = join(PACK_ROOT, "reference-apps", scenario, fileName);
48
+ if (!existsSync(full))
49
+ return null;
50
+ return readFileSync(full, "utf-8");
51
+ }
52
+ function scenarioKeywordsFromName(name) {
53
+ return name
54
+ .toLowerCase()
55
+ .split(/[-_\s]+/)
56
+ .filter(Boolean);
57
+ }
58
+ function parseScenarioManifest(raw) {
59
+ if (!raw)
60
+ return {};
61
+ const result = {};
62
+ const lines = raw.split("\n");
63
+ let currentListKey = null;
64
+ for (const line of lines) {
65
+ const trimmed = line.trim();
66
+ if (!trimmed || trimmed.startsWith("#"))
67
+ continue;
68
+ const keyMatch = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(trimmed);
69
+ if (keyMatch) {
70
+ const [, key, value] = keyMatch;
71
+ if (value === "") {
72
+ currentListKey = key;
73
+ result[key] = [];
74
+ }
75
+ else {
76
+ currentListKey = null;
77
+ result[key] = value.replace(/^["']|["']$/g, "");
78
+ }
79
+ continue;
80
+ }
81
+ if (currentListKey && trimmed.startsWith("- ")) {
82
+ const arr = result[currentListKey] ?? [];
83
+ arr.push(trimmed.replace(/^- /, "").trim());
84
+ result[currentListKey] = arr;
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+ function textResponse(text) {
90
+ return { content: [{ type: "text", text }] };
91
+ }
92
+ function errorResponse(text) {
93
+ return { content: [{ type: "text", text }], isError: true };
94
+ }
95
+ function formatList(title, items) {
96
+ return `## ${title}\n${items.length ? items.map((i) => `- ${i}`).join("\n") : "- (none)"}`;
97
+ }
98
+ function getScenarioSummary(scenario) {
99
+ const manifest = parseScenarioManifest(readScenarioFile(scenario, "manifest.yaml"));
100
+ const title = typeof manifest.title === "string" ? manifest.title : scenario;
101
+ const category = typeof manifest.category === "string" ? manifest.category : "uncategorized";
102
+ const corePatterns = Array.isArray(manifest.core_patterns) ? manifest.core_patterns : [];
103
+ const defaultPlaybooks = Array.isArray(manifest.default_playbooks) ? manifest.default_playbooks : [];
104
+ const matchKeywords = Array.isArray(manifest.match_keywords) ? manifest.match_keywords : [];
105
+ return {
106
+ scenario,
107
+ title,
108
+ category,
109
+ corePatterns,
110
+ defaultPlaybooks,
111
+ matchKeywords,
112
+ manifest,
113
+ };
114
+ }
115
+ // Negative signals: if the situation contains these words and the scenario
116
+ // is not the right match, suppress the score.
117
+ const SCENARIO_NEGATIVE_SIGNALS = {
118
+ "policy-governed-signing": ["ssh", "sign", "signing", "document", "transaction", "certificate"],
119
+ "git-pr-signing-service": ["git", "commit", "merge", "pr", "pull request", "verified", "github"],
120
+ };
121
+ function scoreScenarioMatch(scenario, situation) {
122
+ const lower = situation.toLowerCase();
123
+ const summary = getScenarioSummary(scenario);
124
+ const manifestKeywords = [
125
+ scenario,
126
+ ...scenarioKeywordsFromName(scenario),
127
+ ...summary.corePatterns.map((p) => String(p).toLowerCase()),
128
+ ...summary.title.toLowerCase().split(/\s+/),
129
+ ...summary.category.toLowerCase().split(/\s+/),
130
+ ];
131
+ const unique = Array.from(new Set(manifestKeywords.filter(Boolean)));
132
+ let score = unique.reduce((acc, kw) => acc + (lower.includes(kw) ? 1 : 0), 0);
133
+ for (const phrase of summary.matchKeywords) {
134
+ if (lower.includes(String(phrase).toLowerCase())) {
135
+ score += 3;
136
+ }
137
+ }
138
+ const requiredSignals = SCENARIO_NEGATIVE_SIGNALS[scenario];
139
+ if (requiredSignals && score > 0) {
140
+ const hasAnySignal = requiredSignals.some((sig) => lower.includes(sig));
141
+ if (!hasAnySignal) {
142
+ score = 0;
143
+ }
144
+ }
145
+ return {
146
+ ...summary,
147
+ score,
148
+ };
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // Content catalogs
152
+ // ---------------------------------------------------------------------------
153
+ const CANON_FILES = listMarkdownFiles("canon");
154
+ const PLAYBOOK_FILES = listMarkdownFiles("playbooks");
155
+ const PROMPT_FILES = listMarkdownFiles("prompts");
156
+ const ADAPTER_FILES = listMarkdownFiles("adapters");
157
+ const SKILL_DIRS = listDirectories("skills");
158
+ const REFERENCE_APP_DIRS = listDirectories("reference-apps");
159
+ // ---------------------------------------------------------------------------
160
+ // Server factory
161
+ // ---------------------------------------------------------------------------
162
+ export function createServer() {
163
+ const server = new McpServer({
164
+ name: "@tideorg/mcp",
165
+ version: "1.9.0",
166
+ });
167
+ // 1. List available content
168
+ server.registerTool("tide_list", {
169
+ description: "List all available content in the Tide agent pack by category",
170
+ inputSchema: {
171
+ category: z
172
+ .enum(["canon", "playbooks", "skills", "prompts", "adapters", "scenarios", "all"])
173
+ .describe("Which category to list, or 'all' for everything"),
174
+ },
175
+ annotations: {
176
+ readOnlyHint: true,
177
+ destructiveHint: false,
178
+ },
179
+ }, async ({ category }) => {
180
+ const sections = {};
181
+ if (category === "all" || category === "canon")
182
+ sections.canon = CANON_FILES;
183
+ if (category === "all" || category === "playbooks")
184
+ sections.playbooks = PLAYBOOK_FILES;
185
+ if (category === "all" || category === "skills")
186
+ sections.skills = SKILL_DIRS;
187
+ if (category === "all" || category === "prompts")
188
+ sections.prompts = PROMPT_FILES;
189
+ if (category === "all" || category === "adapters")
190
+ sections.adapters = ADAPTER_FILES;
191
+ if (category === "all" || category === "scenarios")
192
+ sections.scenarios = REFERENCE_APP_DIRS;
193
+ const text = Object.entries(sections)
194
+ .map(([cat, items]) => formatList(cat, items))
195
+ .join("\n\n");
196
+ return textResponse(text);
197
+ });
198
+ // 2. Read a specific canon file
199
+ server.registerTool("tide_canon", {
200
+ description: "Read a canon file (invariants, anti-patterns, concepts, framework-matrix, feature-mapping, troubleshooting, tidecloak-bootstrap, etc.)",
201
+ inputSchema: { name: z.string().describe(`Canon file name. Available: ${CANON_FILES.join(", ")}`) },
202
+ annotations: {
203
+ readOnlyHint: true,
204
+ destructiveHint: false,
205
+ },
206
+ }, async ({ name }) => {
207
+ const content = readPackFile("canon", name);
208
+ if (!content)
209
+ return errorResponse(`Canon file '${name}' not found. Available: ${CANON_FILES.join(", ")}`);
210
+ return textResponse(content);
211
+ });
212
+ // 3. Read a playbook
213
+ server.registerTool("tide_playbook", {
214
+ description: "Read a step-by-step playbook for a specific Tide task",
215
+ inputSchema: { name: z.string().describe(`Playbook name. Available: ${PLAYBOOK_FILES.join(", ")}`) },
216
+ annotations: {
217
+ readOnlyHint: true,
218
+ destructiveHint: false,
219
+ },
220
+ }, async ({ name }) => {
221
+ const content = readPackFile("playbooks", name);
222
+ if (!content)
223
+ return errorResponse(`Playbook '${name}' not found. Available: ${PLAYBOOK_FILES.join(", ")}`);
224
+ return textResponse(content);
225
+ });
226
+ // 4. Read a skill
227
+ server.registerTool("tide_skill", {
228
+ description: "Read a composable skill definition",
229
+ inputSchema: { name: z.string().describe(`Skill name. Available: ${SKILL_DIRS.join(", ")}`) },
230
+ annotations: {
231
+ readOnlyHint: true,
232
+ destructiveHint: false,
233
+ },
234
+ }, async ({ name }) => {
235
+ const content = readSkill(name);
236
+ if (!content)
237
+ return errorResponse(`Skill '${name}' not found. Available: ${SKILL_DIRS.join(", ")}`);
238
+ return textResponse(content);
239
+ });
240
+ // 5. Read a prompt file
241
+ server.registerTool("tide_prompt", {
242
+ description: "Read a reusable starter prompt from the pack",
243
+ inputSchema: { name: z.string().describe(`Prompt file name. Available: ${PROMPT_FILES.join(", ")}`) },
244
+ annotations: {
245
+ readOnlyHint: true,
246
+ destructiveHint: false,
247
+ },
248
+ }, async ({ name }) => {
249
+ const content = readPackFile("prompts", name);
250
+ if (!content)
251
+ return errorResponse(`Prompt '${name}' not found. Available: ${PROMPT_FILES.join(", ")}`);
252
+ return textResponse(content);
253
+ });
254
+ // 6. Read an adapter file
255
+ server.registerTool("tide_adapter", {
256
+ description: "Read an adapter instruction file (AGENTS, CLAUDE, replit)",
257
+ inputSchema: { name: z.string().describe(`Adapter file name. Available: ${ADAPTER_FILES.join(", ")}`) },
258
+ annotations: {
259
+ readOnlyHint: true,
260
+ destructiveHint: false,
261
+ },
262
+ }, async ({ name }) => {
263
+ const content = readPackFile("adapters", name);
264
+ if (!content)
265
+ return errorResponse(`Adapter '${name}' not found. Available: ${ADAPTER_FILES.join(", ")}`);
266
+ return textResponse(content);
267
+ });
268
+ // 7. List available scenarios
269
+ server.registerTool("tide_list_scenarios", {
270
+ description: "List all available scenario patterns under reference-apps/",
271
+ annotations: {
272
+ readOnlyHint: true,
273
+ destructiveHint: false,
274
+ },
275
+ }, async () => {
276
+ if (REFERENCE_APP_DIRS.length === 0)
277
+ return textResponse("No scenarios found under reference-apps/");
278
+ const lines = REFERENCE_APP_DIRS.map((name) => {
279
+ const summary = getScenarioSummary(name);
280
+ return `- ${summary.scenario} — ${summary.title} [${summary.category}]`;
281
+ });
282
+ return textResponse(`Available scenarios:\n\n${lines.join("\n")}`);
283
+ });
284
+ // 8. Read scenario summary
285
+ server.registerTool("tide_scenario", {
286
+ description: "Read a scenario summary from reference-apps/<scenario>/scenario.md",
287
+ inputSchema: { name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`) },
288
+ annotations: {
289
+ readOnlyHint: true,
290
+ destructiveHint: false,
291
+ },
292
+ }, async ({ name }) => {
293
+ if (!scenarioExists(name))
294
+ return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
295
+ const scenarioContent = readScenarioFile(name, "scenario.md");
296
+ if (!scenarioContent)
297
+ return errorResponse(`Scenario '${name}' exists but scenario.md is missing.`);
298
+ const antiPatterns = readScenarioFile(name, "anti-patterns.md");
299
+ const text = antiPatterns ? `${scenarioContent}\n\n---\n\n## Anti-patterns\n\n${antiPatterns}` : scenarioContent;
300
+ return textResponse(text);
301
+ });
302
+ // 9. Read scenario manifest
303
+ server.registerTool("tide_scenario_manifest", {
304
+ description: "Read a scenario manifest from reference-apps/<scenario>/manifest.yaml",
305
+ inputSchema: { name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`) },
306
+ annotations: {
307
+ readOnlyHint: true,
308
+ destructiveHint: false,
309
+ },
310
+ }, async ({ name }) => {
311
+ if (!scenarioExists(name))
312
+ return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
313
+ const content = readScenarioFile(name, "manifest.yaml");
314
+ if (!content)
315
+ return errorResponse(`Scenario '${name}' exists but manifest.yaml is missing.`);
316
+ return textResponse(content);
317
+ });
318
+ // 10. Read scenario role/policy matrix
319
+ server.registerTool("tide_scenario_roles", {
320
+ description: "Read a scenario role-policy matrix from reference-apps/<scenario>/role-policy-matrix.md",
321
+ inputSchema: { name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`) },
322
+ annotations: {
323
+ readOnlyHint: true,
324
+ destructiveHint: false,
325
+ },
326
+ }, async ({ name }) => {
327
+ if (!scenarioExists(name))
328
+ return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
329
+ const content = readScenarioFile(name, "role-policy-matrix.md");
330
+ if (!content)
331
+ return errorResponse(`Scenario '${name}' exists but role-policy-matrix.md is missing.`);
332
+ return textResponse(content);
333
+ });
334
+ // 11. Read scenario bootstrap sequence
335
+ server.registerTool("tide_scenario_bootstrap", {
336
+ description: "Read a scenario bootstrap sequence from reference-apps/<scenario>/bootstrap-sequence.md",
337
+ inputSchema: { name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`) },
338
+ annotations: {
339
+ readOnlyHint: true,
340
+ destructiveHint: false,
341
+ },
342
+ }, async ({ name }) => {
343
+ if (!scenarioExists(name))
344
+ return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
345
+ const content = readScenarioFile(name, "bootstrap-sequence.md");
346
+ if (!content)
347
+ return errorResponse(`Scenario '${name}' exists but bootstrap-sequence.md is missing.`);
348
+ return textResponse(content);
349
+ });
350
+ // 12. Choose best matching scenario
351
+ server.registerTool("tide_choose_scenario", {
352
+ description: "Match a user request to a known scenario pattern before falling back to generic playbooks",
353
+ inputSchema: { situation: z.string().describe("Describe the app or problem, e.g. 'build an organisation password manager'") },
354
+ annotations: {
355
+ readOnlyHint: true,
356
+ destructiveHint: false,
357
+ },
358
+ }, async ({ situation }) => {
359
+ const matches = REFERENCE_APP_DIRS
360
+ .map((scenario) => scoreScenarioMatch(scenario, situation))
361
+ .filter((m) => m.score > 0)
362
+ .sort((a, b) => b.score - a.score);
363
+ if (matches.length === 0) {
364
+ return textResponse(`No scenario match for "${situation}". Available scenarios:\n${REFERENCE_APP_DIRS.map((s) => `- ${s}`).join("\n")}`);
365
+ }
366
+ const best = matches[0];
367
+ const second = matches.length > 1 ? matches[1] : null;
368
+ const autoSelect = best.score >= 6 && (!second || best.score - second.score >= 3);
369
+ const closeMatches = autoSelect ? [best] : matches.filter((m) => m.score >= best.score * 0.6 && m.score > 0);
370
+ if (closeMatches.length > 1) {
371
+ const lines = closeMatches.map((m) => {
372
+ const dq = typeof m.manifest.discriminating_question === "string" ? m.manifest.discriminating_question : "";
373
+ return `- **${m.scenario}** (${m.title}, score ${m.score})${dq ? `\n Disambiguate: ${dq}` : ""}`;
374
+ });
375
+ return textResponse([
376
+ `Multiple scenarios match "${situation}" (I-17 — resolve before proceeding):`,
377
+ "", ...lines, "",
378
+ `Resolve the ambiguity before selecting a playbook path.`,
379
+ ].join("\n"));
380
+ }
381
+ const defaultPlaybooks = best.defaultPlaybooks;
382
+ return textResponse([
383
+ `Best scenario match: ${best.scenario}`,
384
+ `Title: ${best.title}`,
385
+ `Category: ${best.category}`,
386
+ defaultPlaybooks.length
387
+ ? `Default playbooks:\n${defaultPlaybooks.map((p) => `- ${p}`).join("\n")}`
388
+ : `Default playbooks: not declared in manifest.yaml`,
389
+ `Use tide_scenario_manifest, tide_scenario_roles, and tide_scenario_bootstrap for scenario-specific details.`,
390
+ ].join("\n\n"));
391
+ });
392
+ // 13. Recommend the right playbook
393
+ server.registerTool("tide_choose_playbook", {
394
+ description: "Recommend the right playbook for a given situation",
395
+ inputSchema: { situation: z.string().describe("Describe what the builder wants to do, e.g. 'add login to a new Next.js app'") },
396
+ annotations: {
397
+ readOnlyHint: true,
398
+ destructiveHint: false,
399
+ },
400
+ }, async ({ situation }) => {
401
+ const lower = situation.toLowerCase();
402
+ const scenarioMatches = REFERENCE_APP_DIRS
403
+ .map((scenario) => scoreScenarioMatch(scenario, situation))
404
+ .filter((m) => m.score > 0)
405
+ .sort((a, b) => b.score - a.score);
406
+ if (scenarioMatches.length > 0) {
407
+ const best = scenarioMatches[0];
408
+ const closeMatches = scenarioMatches.filter((m) => m.score >= best.score * 0.6 && m.score > 0);
409
+ if (closeMatches.length > 1) {
410
+ const lines = closeMatches.map((m) => {
411
+ const dq = typeof m.manifest.discriminating_question === "string" ? m.manifest.discriminating_question : "";
412
+ return `- **${m.scenario}** (score ${m.score})${dq ? ` — ${dq}` : ""}`;
413
+ });
414
+ return textResponse([
415
+ `Multiple scenarios match (I-17 — resolve before proceeding):`,
416
+ ...lines, ``, `Resolve the ambiguity before selecting a playbook path.`,
417
+ ].join("\n"));
418
+ }
419
+ const defaultPlaybooks = best.defaultPlaybooks;
420
+ return textResponse([
421
+ `Scenario match: ${best.scenario}`,
422
+ defaultPlaybooks.length
423
+ ? `Recommended playbook sequence:\n${defaultPlaybooks.map((p) => `- ${p}`).join("\n")}`
424
+ : `Scenario matched, but manifest.yaml does not declare default_playbooks.`,
425
+ `Use tide_scenario_manifest, tide_scenario_roles, and tide_scenario_bootstrap for scenario-specific details.`,
426
+ ].join("\n\n"));
427
+ }
428
+ const matches = [];
429
+ const rules = [
430
+ { keywords: ["new", "fresh", "setup", "add login", "add auth", "from scratch"], name: "add-auth-nextjs-fresh", reason: "New app needs Tide auth from scratch" },
431
+ { keywords: ["existing", "retrofit", "already has", "migrate"], name: "add-auth-nextjs-existing", reason: "Existing app needs Tide added" },
432
+ { keywords: ["route", "page guard", "redirect", "protect page"], name: "protect-routes-nextjs", reason: "Client-side route protection (UI gating)" },
433
+ { keywords: ["api", "endpoint", "server", "protect api", "backend"], name: "protect-api-nextjs", reason: "Server-side API protection" },
434
+ { keywords: ["jwt", "dpop", "verify", "token"], name: "verify-jwt-server-side", reason: "Complete JWT + DPoP verification" },
435
+ { keywords: ["rbac", "role", "permission", "admin", "access control"], name: "add-rbac-nextjs", reason: "Role-based access control" },
436
+ { keywords: ["login broken", "hang", "stuck", "blank", "csp"], name: "diagnose-broken-login", reason: "Login diagnostics" },
437
+ { keywords: ["role missing", "claim", "no role", "token empty"], name: "diagnose-missing-roles-or-claims", reason: "Missing roles/claims diagnostics" },
438
+ { keywords: ["deploy", "docker", "container", "tidecloak"], name: "deploy-tidecloak-docker", reason: "Deploy TideCloak instance" },
439
+ { keywords: ["e2ee", "encrypt", "decrypt", "forseti", "vault", "share", "sharing", "shared"], name: "setup-forseti-e2ee", reason: "End-to-end encryption setup" },
440
+ { keywords: ["iga", "approval", "governance", "admin panel"], name: "setup-iga-admin-panel", reason: "IGA admin panel setup" },
441
+ { keywords: ["bootstrap", "realm", "init", "initialize"], name: "bootstrap-realm-from-template", reason: "Bootstrap realm from template" },
442
+ { keywords: ["start", "run tidecloak", "launch"], name: "start-tidecloak-dev", reason: "Start TideCloak dev instance" },
443
+ { keywords: ["hosted", "managed", "skycloak", "cloud", "saas", "no infrastructure", "no infra"], name: "provision-tidecloak-skycloak", reason: "Provision hosted TideCloak via Skycloak (managed, no self-hosting)" },
444
+ ];
445
+ for (const rule of rules) {
446
+ if (rule.keywords.some((kw) => lower.includes(kw))) {
447
+ matches.push({ name: rule.name, reason: rule.reason });
448
+ }
449
+ }
450
+ if (matches.length === 0) {
451
+ return textResponse(`No exact scenario or playbook match for "${situation}". Available playbooks:\n${PLAYBOOK_FILES.map((p) => `- ${p}`).join("\n")}\n\nAvailable scenarios:\n${REFERENCE_APP_DIRS.map((s) => `- ${s}`).join("\n")}\n\nFor a new app, the standard sequence is:\n1. add-auth-nextjs-fresh\n2. protect-routes-nextjs\n3. protect-api-nextjs\n4. verify-jwt-server-side\n5. add-rbac-nextjs`);
452
+ }
453
+ const text = matches.map((m) => `**${m.name}** — ${m.reason}`).join("\n");
454
+ return textResponse(`Recommended playbook(s):\n\n${text}`);
455
+ });
456
+ // 14. Security gap analysis entry point
457
+ server.registerTool("tide_security_analysis", {
458
+ description: "Analyze an EXISTING (possibly non-Tide) system for security gaps and map them to Tide capabilities. Returns the Security Analyst role instructions, the security gap mapping table (SG-01…SG-18), and the runtime-probe procedures. Use this when the user asks 'do a security analysis', 'where is my auth weak', or 'what would Tide change about my security'.",
459
+ inputSchema: {
460
+ include_runtime_probes: z
461
+ .boolean()
462
+ .optional()
463
+ .describe("Include the runtime-confirmation probe procedures (canon/security-runtime-probes.md). Only relevant when the operator is authorized to probe a live target. Defaults to true."),
464
+ },
465
+ annotations: {
466
+ readOnlyHint: true,
467
+ destructiveHint: false,
468
+ },
469
+ }, async ({ include_runtime_probes }) => {
470
+ const skill = readSkill("tide-security-analyst");
471
+ const mapping = readPackFile("canon", "security-gap-mapping");
472
+ const featureMapping = readPackFile("canon", "feature-mapping");
473
+ const runtimeProbes = readPackFile("canon", "security-runtime-probes");
474
+ if (!skill || !mapping) {
475
+ return errorResponse("Security analysis assets missing. Expected skills/tide-security-analyst/SKILL.md and canon/security-gap-mapping.md.");
476
+ }
477
+ const withRuntime = include_runtime_probes !== false;
478
+ return textResponse([
479
+ "# Tide Security Analysis — operating instructions",
480
+ "",
481
+ "You are running a security gap analysis of an EXISTING system. Follow the Security Analyst role below.",
482
+ "Work through the gap mapping (SG-01 … SG-18) exhaustively against the target. Every finding needs a named",
483
+ "trust concentration and evidence with a confidence tag. The out-of-scope section is mandatory.",
484
+ "",
485
+ "Two tiers: run the STATIC sweep always. Run the RUNTIME confirmation tier only with explicit authorization",
486
+ "to test a live target — it is governed by the authorization gate in the runtime-probes doc below.",
487
+ "",
488
+ "---",
489
+ "",
490
+ "## Role: Security Analyst",
491
+ "",
492
+ skill,
493
+ "",
494
+ "---",
495
+ "",
496
+ "## Security Gap Mapping (SG-01 … SG-18)",
497
+ "",
498
+ mapping,
499
+ featureMapping
500
+ ? "\n---\n\n## Feature Mapping (Tide capability sourcing — cite this for replacements)\n\n" + featureMapping
501
+ : "",
502
+ withRuntime && runtimeProbes
503
+ ? "\n---\n\n## Runtime Confirmation Probes (opt-in, authorized targets only)\n\n" + runtimeProbes
504
+ : "",
505
+ ].join("\n"));
506
+ });
507
+ // 15. Hosting options (self-host vs partner-hosted / Skycloak)
508
+ server.registerTool("tide_hosting", {
509
+ description: "Explain where TideCloak can run: self-hosted vs partner-hosted (Skycloak, a managed TideCloak-as-a-service). Returns the hosting decision, the trust model, the Skycloak API reference, and the provisioning playbook. Use when the user asks about a hosted/managed option, not wanting to run their own infrastructure, or 'can someone host TideCloak for us'.",
510
+ annotations: {
511
+ readOnlyHint: true,
512
+ destructiveHint: false,
513
+ },
514
+ }, async () => {
515
+ const hosting = readPackFile("canon", "hosting-options");
516
+ const playbook = readPackFile("playbooks", "provision-tidecloak-skycloak");
517
+ if (!hosting) {
518
+ return errorResponse("Hosting assets missing. Expected canon/hosting-options.md.");
519
+ }
520
+ return textResponse([
521
+ "# Tide Hosting Options",
522
+ "",
523
+ "Where TideCloak runs is an infrastructure choice separate from app integration. Resolve self-host vs",
524
+ "hosted BEFORE bootstrap (I-17). State the honest trust-model caveats to the operator, not just the benefits.",
525
+ "",
526
+ "---",
527
+ "",
528
+ hosting,
529
+ playbook
530
+ ? "\n---\n\n## Provisioning Playbook: Hosted TideCloak via Skycloak\n\n" + playbook
531
+ : "",
532
+ ].join("\n"));
533
+ });
534
+ // 16. Read the gap register
535
+ server.registerTool("tide_gaps", {
536
+ description: "Read the gap register — what is still uncertain or unresolved in the pack",
537
+ annotations: {
538
+ readOnlyHint: true,
539
+ destructiveHint: false,
540
+ },
541
+ }, async () => {
542
+ const full = join(PACK_ROOT, "GAP_REGISTER.md");
543
+ if (!existsSync(full))
544
+ return errorResponse("GAP_REGISTER.md not found");
545
+ return textResponse(readFileSync(full, "utf-8"));
546
+ });
547
+ // -------------------------------------------------------------------------
548
+ // Prompts
549
+ // -------------------------------------------------------------------------
550
+ server.prompt("tide-build-app", "Start building a Tide-protected app from scratch", { framework: z.enum(["nextjs", "react-vite", "vanilla"]).describe("Target framework") }, async ({ framework }) => {
551
+ const adapterContent = readPackFile("adapters", "AGENTS") ?? "";
552
+ const invariantsContent = readPackFile("canon", "invariants") ?? "";
553
+ return {
554
+ messages: [{
555
+ role: "user",
556
+ content: {
557
+ type: "text",
558
+ text: `I want to build a new ${framework} app with Tide authentication, authorization, and encryption.\n\nHere are the operational instructions you must follow:\n\n${adapterContent}\n\n---\n\nHere are the security invariants you must never violate:\n\n${invariantsContent}\n\nIf the request matches a known app pattern, start with tide_choose_scenario. Otherwise use tide_choose_playbook. Then follow the resulting bootstrap and playbook path step by step.`,
559
+ },
560
+ }],
561
+ };
562
+ });
563
+ server.prompt("tide-secure-existing", "Add Tide to an existing app", async () => {
564
+ const promptContent = readPackFile("prompts", "secure-existing-app") ?? "";
565
+ return { messages: [{ role: "user", content: { type: "text", text: promptContent } }] };
566
+ });
567
+ server.prompt("tide-security-analysis", "Analyze an existing system for security gaps and map them to Tide", async () => {
568
+ const promptContent = readPackFile("prompts", "security-gap-analysis") ?? "";
569
+ const skill = readSkill("tide-security-analyst") ?? "";
570
+ const mapping = readPackFile("canon", "security-gap-mapping") ?? "";
571
+ const runtimeProbes = readPackFile("canon", "security-runtime-probes") ?? "";
572
+ return {
573
+ messages: [{
574
+ role: "user",
575
+ content: {
576
+ type: "text",
577
+ text: `${promptContent}\n\n---\n\nSecurity Analyst role instructions:\n\n${skill}\n\n---\n\nSecurity gap mapping:\n\n${mapping}\n\n---\n\nRuntime confirmation probes (authorized targets only):\n\n${runtimeProbes}`,
578
+ },
579
+ }],
580
+ };
581
+ });
582
+ server.prompt("tide-build-from-scenario", "Start building a Tide app from a known scenario pattern", {
583
+ scenario: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`),
584
+ framework: z.enum(["nextjs", "react-vite", "vanilla"]).describe("Target framework"),
585
+ }, async ({ scenario, framework }) => {
586
+ if (!scenarioExists(scenario))
587
+ throw new Error(`Scenario '${scenario}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
588
+ const adapterContent = readPackFile("adapters", "AGENTS") ?? "";
589
+ const invariantsContent = readPackFile("canon", "invariants") ?? "";
590
+ const scenarioContent = readScenarioFile(scenario, "scenario.md") ?? "";
591
+ const manifestContent = readScenarioFile(scenario, "manifest.yaml") ?? "";
592
+ const rolesContent = readScenarioFile(scenario, "role-policy-matrix.md") ?? "";
593
+ const bootstrapContent = readScenarioFile(scenario, "bootstrap-sequence.md") ?? "";
594
+ return {
595
+ messages: [{
596
+ role: "user",
597
+ content: {
598
+ type: "text",
599
+ text: `I want to build a ${framework} Tide app using the scenario pattern '${scenario}'.\n\nFollow these adapter instructions first:\n\n${adapterContent}\n\n---\n\nSecurity invariants:\n\n${invariantsContent}\n\n---\n\nScenario summary:\n\n${scenarioContent}\n\n---\n\nScenario manifest:\n\n${manifestContent}\n\n---\n\nRole and policy matrix:\n\n${rolesContent}\n\n---\n\nBootstrap sequence:\n\n${bootstrapContent}\n\nStart by honoring the scenario bootstrap and role/policy requirements before falling back to generic playbook selection.`,
600
+ },
601
+ }],
602
+ };
603
+ });
604
+ server.prompt("tide-mcp-qa", "Run the pre-release QA gate on the Tide MCP pack and issue a SHIP/BLOCK verdict", async () => {
605
+ const skill = readSkill("tide-mcp-qa") ?? "";
606
+ return {
607
+ messages: [{
608
+ role: "user",
609
+ content: {
610
+ type: "text",
611
+ text: `Act as the MCP QA Engineer and decide whether the Tide MCP pack is safe to release.\n\nFollow the role below. First run the deterministic gate: \`cd mcp-server && npm test\` — a red or crashed gate is an automatic BLOCK. Then do the semantic review and honesty audit the gate cannot see, drive a sample of eval cases, and emit the Release Readiness Report with a single verdict (SHIP / SHIP_WITH_WARNINGS / BLOCK). Do not edit tests or doctrine to make a check pass.\n\n---\n\n${skill}`,
612
+ },
613
+ }],
614
+ };
615
+ });
616
+ return server;
617
+ }