@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
@@ -1,605 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from "zod";
5
- import { readFileSync, readdirSync, existsSync } from "fs";
6
- import { join, resolve } from "path";
7
- // Resolve pack root.
8
- // - TIDE_PACK_ROOT env var takes priority (custom deployments)
9
- // - Default: two levels up from dist/index.js → repo root (works for both
10
- // local dev and npm install, since the package.json "files" field places
11
- // content directories at the package root alongside mcp-server/dist/)
12
- const PACK_ROOT = process.env.TIDE_PACK_ROOT
13
- ? resolve(process.env.TIDE_PACK_ROOT)
14
- : resolve(import.meta.dirname, "..", "..");
15
- // ---------------------------------------------------------------------------
16
- // Helpers
17
- // ---------------------------------------------------------------------------
18
- function listMarkdownFiles(dir) {
19
- const full = join(PACK_ROOT, dir);
20
- if (!existsSync(full))
21
- return [];
22
- return readdirSync(full)
23
- .filter((f) => f.endsWith(".md"))
24
- .map((f) => f.replace(/\.md$/, ""));
25
- }
26
- function listDirectories(dir) {
27
- const full = join(PACK_ROOT, dir);
28
- if (!existsSync(full))
29
- return [];
30
- return readdirSync(full, { withFileTypes: true })
31
- .filter((d) => d.isDirectory())
32
- .map((d) => d.name);
33
- }
34
- function readPackFile(dir, name) {
35
- const file = name.endsWith(".md") ? name : `${name}.md`;
36
- const full = join(PACK_ROOT, dir, file);
37
- if (!existsSync(full))
38
- return null;
39
- return readFileSync(full, "utf-8");
40
- }
41
- function readSkill(name) {
42
- const full = join(PACK_ROOT, "skills", name, "SKILL.md");
43
- if (!existsSync(full))
44
- return null;
45
- return readFileSync(full, "utf-8");
46
- }
47
- function scenarioExists(scenario) {
48
- return existsSync(join(PACK_ROOT, "reference-apps", scenario));
49
- }
50
- function readScenarioFile(scenario, fileName) {
51
- const full = join(PACK_ROOT, "reference-apps", scenario, fileName);
52
- if (!existsSync(full))
53
- return null;
54
- return readFileSync(full, "utf-8");
55
- }
56
- function scenarioKeywordsFromName(name) {
57
- return name
58
- .toLowerCase()
59
- .split(/[-_\s]+/)
60
- .filter(Boolean);
61
- }
62
- function parseScenarioManifest(raw) {
63
- if (!raw)
64
- return {};
65
- const result = {};
66
- const lines = raw.split("\n");
67
- let currentListKey = null;
68
- for (const line of lines) {
69
- const trimmed = line.trim();
70
- if (!trimmed || trimmed.startsWith("#"))
71
- continue;
72
- const keyMatch = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(trimmed);
73
- if (keyMatch) {
74
- const [, key, value] = keyMatch;
75
- if (value === "") {
76
- currentListKey = key;
77
- result[key] = [];
78
- }
79
- else {
80
- currentListKey = null;
81
- result[key] = value.replace(/^["']|["']$/g, "");
82
- }
83
- continue;
84
- }
85
- if (currentListKey && trimmed.startsWith("- ")) {
86
- const arr = result[currentListKey] ?? [];
87
- arr.push(trimmed.replace(/^- /, "").trim());
88
- result[currentListKey] = arr;
89
- }
90
- }
91
- return result;
92
- }
93
- function textResponse(text) {
94
- return { content: [{ type: "text", text }] };
95
- }
96
- function errorResponse(text) {
97
- return { content: [{ type: "text", text }], isError: true };
98
- }
99
- function formatList(title, items) {
100
- return `## ${title}\n${items.length ? items.map((i) => `- ${i}`).join("\n") : "- (none)"}`;
101
- }
102
- function getScenarioSummary(scenario) {
103
- const manifest = parseScenarioManifest(readScenarioFile(scenario, "manifest.yaml"));
104
- const title = typeof manifest.title === "string" ? manifest.title : scenario;
105
- const category = typeof manifest.category === "string" ? manifest.category : "uncategorized";
106
- const corePatterns = Array.isArray(manifest.core_patterns) ? manifest.core_patterns : [];
107
- const defaultPlaybooks = Array.isArray(manifest.default_playbooks) ? manifest.default_playbooks : [];
108
- const matchKeywords = Array.isArray(manifest.match_keywords) ? manifest.match_keywords : [];
109
- return {
110
- scenario,
111
- title,
112
- category,
113
- corePatterns,
114
- defaultPlaybooks,
115
- matchKeywords,
116
- manifest,
117
- };
118
- }
119
- // Negative signals: if the situation contains these words and the scenario
120
- // is not the right match, suppress the score. Prevents false positives
121
- // like "policy-governed-signing" matching on generic encryption requests.
122
- const SCENARIO_NEGATIVE_SIGNALS = {
123
- "policy-governed-signing": ["ssh", "sign", "signing", "document", "transaction", "certificate"],
124
- "git-pr-signing-service": ["git", "commit", "merge", "pr", "pull request", "verified", "github"],
125
- };
126
- function scoreScenarioMatch(scenario, situation) {
127
- const lower = situation.toLowerCase();
128
- const summary = getScenarioSummary(scenario);
129
- // Single-word keyword matching (core_patterns, title words, category words, scenario name parts)
130
- const manifestKeywords = [
131
- scenario,
132
- ...scenarioKeywordsFromName(scenario),
133
- ...summary.corePatterns.map((p) => String(p).toLowerCase()),
134
- ...summary.title.toLowerCase().split(/\s+/),
135
- ...summary.category.toLowerCase().split(/\s+/),
136
- ];
137
- const unique = Array.from(new Set(manifestKeywords.filter(Boolean)));
138
- let score = unique.reduce((acc, kw) => acc + (lower.includes(kw) ? 1 : 0), 0);
139
- // Multi-word phrase matching from match_keywords (weighted higher — phrase match is stronger signal)
140
- for (const phrase of summary.matchKeywords) {
141
- if (lower.includes(String(phrase).toLowerCase())) {
142
- score += 3;
143
- }
144
- }
145
- // Negative-signal suppression: if a scenario requires domain-specific
146
- // signals (e.g., signing/SSH) and none are present in the request,
147
- // suppress the match. Prevents "policy-governed-signing" from matching
148
- // generic encryption/sharing requests. (LEARNINGS-session-003 L-05)
149
- const requiredSignals = SCENARIO_NEGATIVE_SIGNALS[scenario];
150
- if (requiredSignals && score > 0) {
151
- const hasAnySignal = requiredSignals.some((sig) => lower.includes(sig));
152
- if (!hasAnySignal) {
153
- score = 0;
154
- }
155
- }
156
- return {
157
- ...summary,
158
- score,
159
- };
160
- }
161
- // ---------------------------------------------------------------------------
162
- // Content catalogs
163
- // ---------------------------------------------------------------------------
164
- const CANON_FILES = listMarkdownFiles("canon");
165
- const PLAYBOOK_FILES = listMarkdownFiles("playbooks");
166
- const PROMPT_FILES = listMarkdownFiles("prompts");
167
- const ADAPTER_FILES = listMarkdownFiles("adapters");
168
- const SKILL_DIRS = listDirectories("skills");
169
- const REFERENCE_APP_DIRS = listDirectories("reference-apps");
170
- // ---------------------------------------------------------------------------
171
- // Server
172
- // ---------------------------------------------------------------------------
173
- const server = new McpServer({
174
- name: "@tideorg/mcp",
175
- version: "1.4.0",
176
- });
177
- // ---------------------------------------------------------------------------
178
- // Tools
179
- // ---------------------------------------------------------------------------
180
- // 1. List available content
181
- server.tool("tide_list", "List all available content in the Tide agent pack by category", {
182
- category: z
183
- .enum(["canon", "playbooks", "skills", "prompts", "adapters", "scenarios", "all"])
184
- .describe("Which category to list, or 'all' for everything"),
185
- }, async ({ category }) => {
186
- const sections = {};
187
- if (category === "all" || category === "canon")
188
- sections.canon = CANON_FILES;
189
- if (category === "all" || category === "playbooks")
190
- sections.playbooks = PLAYBOOK_FILES;
191
- if (category === "all" || category === "skills")
192
- sections.skills = SKILL_DIRS;
193
- if (category === "all" || category === "prompts")
194
- sections.prompts = PROMPT_FILES;
195
- if (category === "all" || category === "adapters")
196
- sections.adapters = ADAPTER_FILES;
197
- if (category === "all" || category === "scenarios")
198
- sections.scenarios = REFERENCE_APP_DIRS;
199
- const text = Object.entries(sections)
200
- .map(([cat, items]) => formatList(cat, items))
201
- .join("\n\n");
202
- return textResponse(text);
203
- });
204
- // 2. Read a specific canon file
205
- server.tool("tide_canon", "Read a canon file (invariants, anti-patterns, concepts, framework-matrix, feature-mapping, troubleshooting, tidecloak-bootstrap, etc.)", {
206
- name: z.string().describe(`Canon file name. Available: ${CANON_FILES.join(", ")}`),
207
- }, async ({ name }) => {
208
- const content = readPackFile("canon", name);
209
- if (!content) {
210
- return errorResponse(`Canon file '${name}' not found. Available: ${CANON_FILES.join(", ")}`);
211
- }
212
- return textResponse(content);
213
- });
214
- // 3. Read a playbook
215
- server.tool("tide_playbook", "Read a step-by-step playbook for a specific Tide task", {
216
- name: z.string().describe(`Playbook name. Available: ${PLAYBOOK_FILES.join(", ")}`),
217
- }, async ({ name }) => {
218
- const content = readPackFile("playbooks", name);
219
- if (!content) {
220
- return errorResponse(`Playbook '${name}' not found. Available: ${PLAYBOOK_FILES.join(", ")}`);
221
- }
222
- return textResponse(content);
223
- });
224
- // 4. Read a skill
225
- server.tool("tide_skill", "Read a composable skill definition", {
226
- name: z.string().describe(`Skill name. Available: ${SKILL_DIRS.join(", ")}`),
227
- }, async ({ name }) => {
228
- const content = readSkill(name);
229
- if (!content) {
230
- return errorResponse(`Skill '${name}' not found. Available: ${SKILL_DIRS.join(", ")}`);
231
- }
232
- return textResponse(content);
233
- });
234
- // 5. Read a prompt file
235
- server.tool("tide_prompt", "Read a reusable starter prompt from the pack", {
236
- name: z.string().describe(`Prompt file name. Available: ${PROMPT_FILES.join(", ")}`),
237
- }, async ({ name }) => {
238
- const content = readPackFile("prompts", name);
239
- if (!content) {
240
- return errorResponse(`Prompt '${name}' not found. Available: ${PROMPT_FILES.join(", ")}`);
241
- }
242
- return textResponse(content);
243
- });
244
- // 6. Read an adapter file
245
- server.tool("tide_adapter", "Read an adapter instruction file (AGENTS, CLAUDE, replit)", {
246
- name: z.string().describe(`Adapter file name. Available: ${ADAPTER_FILES.join(", ")}`),
247
- }, async ({ name }) => {
248
- const content = readPackFile("adapters", name);
249
- if (!content) {
250
- return errorResponse(`Adapter '${name}' not found. Available: ${ADAPTER_FILES.join(", ")}`);
251
- }
252
- return textResponse(content);
253
- });
254
- // 7. List available scenarios
255
- server.tool("tide_list_scenarios", "List all available scenario patterns under reference-apps/", async () => {
256
- if (REFERENCE_APP_DIRS.length === 0) {
257
- return textResponse("No scenarios found under reference-apps/");
258
- }
259
- const lines = REFERENCE_APP_DIRS.map((name) => {
260
- const summary = getScenarioSummary(name);
261
- return `- ${summary.scenario} — ${summary.title} [${summary.category}]`;
262
- });
263
- return textResponse(`Available scenarios:\n\n${lines.join("\n")}`);
264
- });
265
- // 8. Read scenario summary
266
- server.tool("tide_scenario", "Read a scenario summary from reference-apps/<scenario>/scenario.md", {
267
- name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`),
268
- }, async ({ name }) => {
269
- if (!scenarioExists(name)) {
270
- return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
271
- }
272
- const scenarioContent = readScenarioFile(name, "scenario.md");
273
- if (!scenarioContent) {
274
- return errorResponse(`Scenario '${name}' exists but scenario.md is missing.`);
275
- }
276
- const antiPatterns = readScenarioFile(name, "anti-patterns.md");
277
- const text = antiPatterns
278
- ? `${scenarioContent}\n\n---\n\n## Anti-patterns\n\n${antiPatterns}`
279
- : scenarioContent;
280
- return textResponse(text);
281
- });
282
- // 9. Read scenario manifest
283
- server.tool("tide_scenario_manifest", "Read a scenario manifest from reference-apps/<scenario>/manifest.yaml", {
284
- name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`),
285
- }, async ({ name }) => {
286
- if (!scenarioExists(name)) {
287
- return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
288
- }
289
- const content = readScenarioFile(name, "manifest.yaml");
290
- if (!content) {
291
- return errorResponse(`Scenario '${name}' exists but manifest.yaml is missing.`);
292
- }
293
- return textResponse(content);
294
- });
295
- // 10. Read scenario role/policy matrix
296
- server.tool("tide_scenario_roles", "Read a scenario role-policy matrix from reference-apps/<scenario>/role-policy-matrix.md", {
297
- name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`),
298
- }, async ({ name }) => {
299
- if (!scenarioExists(name)) {
300
- return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
301
- }
302
- const content = readScenarioFile(name, "role-policy-matrix.md");
303
- if (!content) {
304
- return errorResponse(`Scenario '${name}' exists but role-policy-matrix.md is missing.`);
305
- }
306
- return textResponse(content);
307
- });
308
- // 11. Read scenario bootstrap sequence
309
- server.tool("tide_scenario_bootstrap", "Read a scenario bootstrap sequence from reference-apps/<scenario>/bootstrap-sequence.md", {
310
- name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`),
311
- }, async ({ name }) => {
312
- if (!scenarioExists(name)) {
313
- return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
314
- }
315
- const content = readScenarioFile(name, "bootstrap-sequence.md");
316
- if (!content) {
317
- return errorResponse(`Scenario '${name}' exists but bootstrap-sequence.md is missing.`);
318
- }
319
- return textResponse(content);
320
- });
321
- // 12. Choose best matching scenario
322
- server.tool("tide_choose_scenario", "Match a user request to a known scenario pattern before falling back to generic playbooks", {
323
- situation: z.string().describe("Describe the app or problem, e.g. 'build an organisation password manager'"),
324
- }, async ({ situation }) => {
325
- const matches = REFERENCE_APP_DIRS
326
- .map((scenario) => scoreScenarioMatch(scenario, situation))
327
- .filter((m) => m.score > 0)
328
- .sort((a, b) => b.score - a.score);
329
- if (matches.length === 0) {
330
- return textResponse(`No scenario match for "${situation}". Available scenarios:\n${REFERENCE_APP_DIRS.map((s) => `- ${s}`).join("\n")}`);
331
- }
332
- // I-17: If multiple scenarios score equally or close, surface ambiguity.
333
- // Auto-select if the top score is high enough and the gap to second is clear.
334
- // (LEARNINGS-batch-007 L-02: score 9 vs 7 should auto-select, not disambiguate)
335
- const best = matches[0];
336
- const second = matches.length > 1 ? matches[1] : null;
337
- const autoSelect = best.score >= 6 && (!second || best.score - second.score >= 3);
338
- const closeMatches = autoSelect
339
- ? [best]
340
- : matches.filter((m) => m.score >= best.score * 0.6 && m.score > 0);
341
- if (closeMatches.length > 1) {
342
- const lines = closeMatches.map((m) => {
343
- const dq = typeof m.manifest.discriminating_question === "string"
344
- ? m.manifest.discriminating_question
345
- : "";
346
- return `- **${m.scenario}** (${m.title}, score ${m.score})${dq ? `\n Disambiguate: ${dq}` : ""}`;
347
- });
348
- return textResponse([
349
- `Multiple scenarios match "${situation}" (I-17 — resolve before proceeding):`,
350
- "",
351
- ...lines,
352
- "",
353
- `Resolve the ambiguity before selecting a playbook path. Use the discriminating questions above or check repo evidence.`,
354
- ].join("\n"));
355
- }
356
- const defaultPlaybooks = best.defaultPlaybooks;
357
- const text = [
358
- `Best scenario match: ${best.scenario}`,
359
- `Title: ${best.title}`,
360
- `Category: ${best.category}`,
361
- defaultPlaybooks.length
362
- ? `Default playbooks:\n${defaultPlaybooks.map((p) => `- ${p}`).join("\n")}`
363
- : `Default playbooks: not declared in manifest.yaml`,
364
- `Use tide_scenario_manifest, tide_scenario_roles, and tide_scenario_bootstrap for scenario-specific details.`,
365
- ].join("\n\n");
366
- return textResponse(text);
367
- });
368
- // 13. Recommend the right playbook, scenario-first
369
- server.tool("tide_choose_playbook", "Recommend the right playbook for a given situation", {
370
- situation: z.string().describe("Describe what the builder wants to do, e.g. 'add login to a new Next.js app'"),
371
- }, async ({ situation }) => {
372
- const lower = situation.toLowerCase();
373
- // ---------------------------------------------------------------------
374
- // First: scenario matching
375
- // ---------------------------------------------------------------------
376
- const scenarioMatches = REFERENCE_APP_DIRS
377
- .map((scenario) => scoreScenarioMatch(scenario, situation))
378
- .filter((m) => m.score > 0)
379
- .sort((a, b) => b.score - a.score);
380
- if (scenarioMatches.length > 0) {
381
- const best = scenarioMatches[0];
382
- const closeMatches = scenarioMatches.filter((m) => m.score >= best.score * 0.6 && m.score > 0);
383
- // I-17: Surface ambiguity if multiple scenarios match closely
384
- if (closeMatches.length > 1) {
385
- const lines = closeMatches.map((m) => {
386
- const dq = typeof m.manifest.discriminating_question === "string"
387
- ? m.manifest.discriminating_question
388
- : "";
389
- return `- **${m.scenario}** (score ${m.score})${dq ? ` — ${dq}` : ""}`;
390
- });
391
- return textResponse([
392
- `Multiple scenarios match (I-17 — resolve before proceeding):`,
393
- ...lines,
394
- ``,
395
- `Resolve the ambiguity before selecting a playbook path.`,
396
- ].join("\n"));
397
- }
398
- const defaultPlaybooks = best.defaultPlaybooks;
399
- return textResponse([
400
- `Scenario match: ${best.scenario}`,
401
- defaultPlaybooks.length
402
- ? `Recommended playbook sequence:\n${defaultPlaybooks.map((p) => `- ${p}`).join("\n")}`
403
- : `Scenario matched, but manifest.yaml does not declare default_playbooks.`,
404
- `Use tide_scenario_manifest, tide_scenario_roles, and tide_scenario_bootstrap for scenario-specific details.`,
405
- ].join("\n\n"));
406
- }
407
- // ---------------------------------------------------------------------
408
- // Second: generic keyword routing
409
- // ---------------------------------------------------------------------
410
- const matches = [];
411
- const rules = [
412
- {
413
- keywords: ["new", "fresh", "setup", "add login", "add auth", "from scratch"],
414
- name: "add-auth-nextjs-fresh",
415
- reason: "New app needs Tide auth from scratch",
416
- },
417
- {
418
- keywords: ["existing", "retrofit", "already has", "migrate"],
419
- name: "add-auth-nextjs-existing",
420
- reason: "Existing app needs Tide added",
421
- },
422
- {
423
- keywords: ["route", "page guard", "redirect", "protect page"],
424
- name: "protect-routes-nextjs",
425
- reason: "Client-side route protection (UI gating)",
426
- },
427
- {
428
- keywords: ["api", "endpoint", "server", "protect api", "backend"],
429
- name: "protect-api-nextjs",
430
- reason: "Server-side API protection",
431
- },
432
- {
433
- keywords: ["jwt", "dpop", "verify", "token"],
434
- name: "verify-jwt-server-side",
435
- reason: "Complete JWT + DPoP verification",
436
- },
437
- {
438
- keywords: ["rbac", "role", "permission", "admin", "access control"],
439
- name: "add-rbac-nextjs",
440
- reason: "Role-based access control",
441
- },
442
- {
443
- keywords: ["login broken", "hang", "stuck", "blank", "csp"],
444
- name: "diagnose-broken-login",
445
- reason: "Login diagnostics",
446
- },
447
- {
448
- keywords: ["role missing", "claim", "no role", "token empty"],
449
- name: "diagnose-missing-roles-or-claims",
450
- reason: "Missing roles/claims diagnostics",
451
- },
452
- {
453
- keywords: ["deploy", "docker", "container", "tidecloak"],
454
- name: "deploy-tidecloak-docker",
455
- reason: "Deploy TideCloak instance",
456
- },
457
- {
458
- keywords: ["e2ee", "encrypt", "decrypt", "forseti", "vault", "share", "sharing", "shared"],
459
- name: "setup-forseti-e2ee",
460
- reason: "End-to-end encryption setup (includes shared/policy-governed encryption)",
461
- },
462
- {
463
- keywords: ["iga", "approval", "governance", "admin panel"],
464
- name: "setup-iga-admin-panel",
465
- reason: "IGA admin panel setup",
466
- },
467
- {
468
- keywords: ["bootstrap", "realm", "init", "initialize"],
469
- name: "bootstrap-realm-from-template",
470
- reason: "Bootstrap realm from template",
471
- },
472
- {
473
- keywords: ["start", "run tidecloak", "launch"],
474
- name: "start-tidecloak-dev",
475
- reason: "Start TideCloak dev instance",
476
- },
477
- ];
478
- for (const rule of rules) {
479
- if (rule.keywords.some((kw) => lower.includes(kw))) {
480
- matches.push({ name: rule.name, reason: rule.reason });
481
- }
482
- }
483
- if (matches.length === 0) {
484
- 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`);
485
- }
486
- const text = matches.map((m) => `**${m.name}** — ${m.reason}`).join("\n");
487
- return textResponse(`Recommended playbook(s):\n\n${text}`);
488
- });
489
- // 14. Read the gap register
490
- server.tool("tide_gaps", "Read the gap register — what is still uncertain or unresolved in the pack", async () => {
491
- const full = join(PACK_ROOT, "GAP_REGISTER.md");
492
- if (!existsSync(full)) {
493
- return errorResponse("GAP_REGISTER.md not found");
494
- }
495
- return textResponse(readFileSync(full, "utf-8"));
496
- });
497
- // ---------------------------------------------------------------------------
498
- // Prompts (MCP prompts — pre-built conversation starters)
499
- // ---------------------------------------------------------------------------
500
- server.prompt("tide-build-app", "Start building a Tide-protected app from scratch", {
501
- framework: z.enum(["nextjs", "react-vite", "vanilla"]).describe("Target framework"),
502
- }, async ({ framework }) => {
503
- const adapterContent = readPackFile("adapters", "AGENTS") ?? "";
504
- const invariantsContent = readPackFile("canon", "invariants") ?? "";
505
- return {
506
- messages: [
507
- {
508
- role: "user",
509
- content: {
510
- type: "text",
511
- text: `I want to build a new ${framework} app with Tide authentication, authorization, and encryption.
512
-
513
- Here are the operational instructions you must follow:
514
-
515
- ${adapterContent}
516
-
517
- ---
518
-
519
- Here are the security invariants you must never violate:
520
-
521
- ${invariantsContent}
522
-
523
- If 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.`,
524
- },
525
- },
526
- ],
527
- };
528
- });
529
- server.prompt("tide-secure-existing", "Add Tide to an existing app", async () => {
530
- const promptContent = readPackFile("prompts", "secure-existing-app") ?? "";
531
- return {
532
- messages: [
533
- {
534
- role: "user",
535
- content: { type: "text", text: promptContent },
536
- },
537
- ],
538
- };
539
- });
540
- server.prompt("tide-build-from-scenario", "Start building a Tide app from a known scenario pattern", {
541
- scenario: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`),
542
- framework: z.enum(["nextjs", "react-vite", "vanilla"]).describe("Target framework"),
543
- }, async ({ scenario, framework }) => {
544
- if (!scenarioExists(scenario)) {
545
- throw new Error(`Scenario '${scenario}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
546
- }
547
- const adapterContent = readPackFile("adapters", "AGENTS") ?? "";
548
- const invariantsContent = readPackFile("canon", "invariants") ?? "";
549
- const scenarioContent = readScenarioFile(scenario, "scenario.md") ?? "";
550
- const manifestContent = readScenarioFile(scenario, "manifest.yaml") ?? "";
551
- const rolesContent = readScenarioFile(scenario, "role-policy-matrix.md") ?? "";
552
- const bootstrapContent = readScenarioFile(scenario, "bootstrap-sequence.md") ?? "";
553
- return {
554
- messages: [
555
- {
556
- role: "user",
557
- content: {
558
- type: "text",
559
- text: `I want to build a ${framework} Tide app using the scenario pattern '${scenario}'.
560
-
561
- Follow these adapter instructions first:
562
-
563
- ${adapterContent}
564
-
565
- ---
566
-
567
- Security invariants:
568
-
569
- ${invariantsContent}
570
-
571
- ---
572
-
573
- Scenario summary:
574
-
575
- ${scenarioContent}
576
-
577
- ---
578
-
579
- Scenario manifest:
580
-
581
- ${manifestContent}
582
-
583
- ---
584
-
585
- Role and policy matrix:
586
-
587
- ${rolesContent}
588
-
589
- ---
590
-
591
- Bootstrap sequence:
592
-
593
- ${bootstrapContent}
594
-
595
- Start by honoring the scenario bootstrap and role/policy requirements before falling back to generic playbook selection.`,
596
- },
597
- },
598
- ],
599
- };
600
- });
601
- // ---------------------------------------------------------------------------
602
- // Start
603
- // ---------------------------------------------------------------------------
3
+ import { createServer } from "./server.js";
4
+ const server = createServer();
604
5
  const transport = new StdioServerTransport();
605
6
  await server.connect(transport);
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;