@tideorg/mcp 1.4.2 → 1.9.1

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 (57) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/GAP_REGISTER.md +18 -8
  3. package/PRIVACY.md +41 -0
  4. package/README.md +207 -197
  5. package/adapters/AGENTS.md +3 -2
  6. package/adapters/CLAUDE.md +1 -1
  7. package/canon/anti-patterns.md +55 -62
  8. package/canon/concepts.md +46 -34
  9. package/canon/custom-contracts.md +12 -8
  10. package/canon/feature-mapping.md +27 -75
  11. package/canon/framework-matrix.md +69 -22
  12. package/canon/hosting-options.md +111 -0
  13. package/canon/iga-change-requests-api.md +211 -0
  14. package/canon/invariants.md +33 -35
  15. package/canon/security-gap-mapping.md +506 -0
  16. package/canon/security-runtime-probes.md +177 -0
  17. package/canon/tidecloak-bootstrap.md +21 -16
  18. package/canon/tidecloak-endpoints.md +60 -98
  19. package/canon/troubleshooting.md +115 -53
  20. package/canon/version-policy.md +8 -12
  21. package/mcp-server/dist/server.js +221 -19
  22. package/package.json +48 -45
  23. package/playbooks/add-auth-nextjs-existing.md +33 -17
  24. package/playbooks/add-auth-nextjs-fresh.md +1 -1
  25. package/playbooks/add-rbac-nextjs.md +7 -2
  26. package/playbooks/bootstrap-realm-from-template.md +33 -18
  27. package/playbooks/deploy-tidecloak-docker.md +31 -28
  28. package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
  29. package/playbooks/initialize-admin-and-link-account.md +22 -19
  30. package/playbooks/protect-api-nextjs.md +40 -1
  31. package/playbooks/protect-aspnet-core-asgard.md +313 -0
  32. package/playbooks/protect-routes-nextjs.md +24 -10
  33. package/playbooks/provision-tidecloak-skycloak.md +213 -0
  34. package/playbooks/setup-forseti-e2ee.md +32 -50
  35. package/playbooks/setup-iga-admin-panel.md +113 -171
  36. package/playbooks/verify-jwt-server-side.md +20 -1
  37. package/prompts/build-private-customer-portal.md +1 -1
  38. package/prompts/security-gap-analysis.md +55 -0
  39. package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
  40. package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
  41. package/reference-apps/encrypted-communication/scenario.md +2 -2
  42. package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
  43. package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
  44. package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
  45. package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
  46. package/reference-apps/iga-admin-governance/scenario.md +15 -13
  47. package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
  48. package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
  49. package/skills/tide-diagnostics/SKILL.md +1 -1
  50. package/skills/tide-integration/SKILL.md +9 -18
  51. package/skills/tide-mcp-qa/SKILL.md +139 -0
  52. package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
  53. package/skills/tide-reviewer/SKILL.md +1 -1
  54. package/skills/tide-security-analyst/SKILL.md +182 -0
  55. package/skills/tide-setup/SKILL.md +1 -1
  56. package/canon/delegation.md +0 -195
  57. package/playbooks/setup-server-delegation.md +0 -142
@@ -162,13 +162,20 @@ const REFERENCE_APP_DIRS = listDirectories("reference-apps");
162
162
  export function createServer() {
163
163
  const server = new McpServer({
164
164
  name: "@tideorg/mcp",
165
- version: "1.4.1",
165
+ version: "1.9.1",
166
166
  });
167
167
  // 1. List available content
168
- server.tool("tide_list", "List all available content in the Tide agent pack by category", {
169
- category: z
170
- .enum(["canon", "playbooks", "skills", "prompts", "adapters", "scenarios", "all"])
171
- .describe("Which category to list, or 'all' for everything"),
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
+ },
172
179
  }, async ({ category }) => {
173
180
  const sections = {};
174
181
  if (category === "all" || category === "canon")
@@ -189,42 +196,83 @@ export function createServer() {
189
196
  return textResponse(text);
190
197
  });
191
198
  // 2. Read a specific canon file
192
- server.tool("tide_canon", "Read a canon file (invariants, anti-patterns, concepts, framework-matrix, feature-mapping, troubleshooting, tidecloak-bootstrap, etc.)", { name: z.string().describe(`Canon file name. Available: ${CANON_FILES.join(", ")}`) }, async ({ name }) => {
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 }) => {
193
207
  const content = readPackFile("canon", name);
194
208
  if (!content)
195
209
  return errorResponse(`Canon file '${name}' not found. Available: ${CANON_FILES.join(", ")}`);
196
210
  return textResponse(content);
197
211
  });
198
212
  // 3. Read a playbook
199
- server.tool("tide_playbook", "Read a step-by-step playbook for a specific Tide task", { name: z.string().describe(`Playbook name. Available: ${PLAYBOOK_FILES.join(", ")}`) }, async ({ name }) => {
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 }) => {
200
221
  const content = readPackFile("playbooks", name);
201
222
  if (!content)
202
223
  return errorResponse(`Playbook '${name}' not found. Available: ${PLAYBOOK_FILES.join(", ")}`);
203
224
  return textResponse(content);
204
225
  });
205
226
  // 4. Read a skill
206
- server.tool("tide_skill", "Read a composable skill definition", { name: z.string().describe(`Skill name. Available: ${SKILL_DIRS.join(", ")}`) }, async ({ name }) => {
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 }) => {
207
235
  const content = readSkill(name);
208
236
  if (!content)
209
237
  return errorResponse(`Skill '${name}' not found. Available: ${SKILL_DIRS.join(", ")}`);
210
238
  return textResponse(content);
211
239
  });
212
240
  // 5. Read a prompt file
213
- server.tool("tide_prompt", "Read a reusable starter prompt from the pack", { name: z.string().describe(`Prompt file name. Available: ${PROMPT_FILES.join(", ")}`) }, async ({ name }) => {
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 }) => {
214
249
  const content = readPackFile("prompts", name);
215
250
  if (!content)
216
251
  return errorResponse(`Prompt '${name}' not found. Available: ${PROMPT_FILES.join(", ")}`);
217
252
  return textResponse(content);
218
253
  });
219
254
  // 6. Read an adapter file
220
- server.tool("tide_adapter", "Read an adapter instruction file (AGENTS, CLAUDE, replit)", { name: z.string().describe(`Adapter file name. Available: ${ADAPTER_FILES.join(", ")}`) }, async ({ name }) => {
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 }) => {
221
263
  const content = readPackFile("adapters", name);
222
264
  if (!content)
223
265
  return errorResponse(`Adapter '${name}' not found. Available: ${ADAPTER_FILES.join(", ")}`);
224
266
  return textResponse(content);
225
267
  });
226
268
  // 7. List available scenarios
227
- server.tool("tide_list_scenarios", "List all available scenario patterns under reference-apps/", async () => {
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 () => {
228
276
  if (REFERENCE_APP_DIRS.length === 0)
229
277
  return textResponse("No scenarios found under reference-apps/");
230
278
  const lines = REFERENCE_APP_DIRS.map((name) => {
@@ -234,7 +282,14 @@ export function createServer() {
234
282
  return textResponse(`Available scenarios:\n\n${lines.join("\n")}`);
235
283
  });
236
284
  // 8. Read scenario summary
237
- server.tool("tide_scenario", "Read a scenario summary from reference-apps/<scenario>/scenario.md", { name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`) }, async ({ name }) => {
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 }) => {
238
293
  if (!scenarioExists(name))
239
294
  return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
240
295
  const scenarioContent = readScenarioFile(name, "scenario.md");
@@ -245,7 +300,14 @@ export function createServer() {
245
300
  return textResponse(text);
246
301
  });
247
302
  // 9. Read scenario manifest
248
- server.tool("tide_scenario_manifest", "Read a scenario manifest from reference-apps/<scenario>/manifest.yaml", { name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`) }, async ({ name }) => {
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 }) => {
249
311
  if (!scenarioExists(name))
250
312
  return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
251
313
  const content = readScenarioFile(name, "manifest.yaml");
@@ -254,7 +316,14 @@ export function createServer() {
254
316
  return textResponse(content);
255
317
  });
256
318
  // 10. Read scenario role/policy matrix
257
- server.tool("tide_scenario_roles", "Read a scenario role-policy matrix from reference-apps/<scenario>/role-policy-matrix.md", { name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`) }, async ({ name }) => {
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 }) => {
258
327
  if (!scenarioExists(name))
259
328
  return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
260
329
  const content = readScenarioFile(name, "role-policy-matrix.md");
@@ -263,7 +332,14 @@ export function createServer() {
263
332
  return textResponse(content);
264
333
  });
265
334
  // 11. Read scenario bootstrap sequence
266
- server.tool("tide_scenario_bootstrap", "Read a scenario bootstrap sequence from reference-apps/<scenario>/bootstrap-sequence.md", { name: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`) }, async ({ name }) => {
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 }) => {
267
343
  if (!scenarioExists(name))
268
344
  return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
269
345
  const content = readScenarioFile(name, "bootstrap-sequence.md");
@@ -272,7 +348,14 @@ export function createServer() {
272
348
  return textResponse(content);
273
349
  });
274
350
  // 12. Choose best matching scenario
275
- server.tool("tide_choose_scenario", "Match a user request to a known scenario pattern before falling back to generic playbooks", { situation: z.string().describe("Describe the app or problem, e.g. 'build an organisation password manager'") }, async ({ situation }) => {
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 }) => {
276
359
  const matches = REFERENCE_APP_DIRS
277
360
  .map((scenario) => scoreScenarioMatch(scenario, situation))
278
361
  .filter((m) => m.score > 0)
@@ -307,7 +390,14 @@ export function createServer() {
307
390
  ].join("\n\n"));
308
391
  });
309
392
  // 13. Recommend the right playbook
310
- server.tool("tide_choose_playbook", "Recommend the right playbook for a given situation", { situation: z.string().describe("Describe what the builder wants to do, e.g. 'add login to a new Next.js app'") }, async ({ situation }) => {
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 }) => {
311
401
  const lower = situation.toLowerCase();
312
402
  const scenarioMatches = REFERENCE_APP_DIRS
313
403
  .map((scenario) => scoreScenarioMatch(scenario, situation))
@@ -350,6 +440,7 @@ export function createServer() {
350
440
  { keywords: ["iga", "approval", "governance", "admin panel"], name: "setup-iga-admin-panel", reason: "IGA admin panel setup" },
351
441
  { keywords: ["bootstrap", "realm", "init", "initialize"], name: "bootstrap-realm-from-template", reason: "Bootstrap realm from template" },
352
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)" },
353
444
  ];
354
445
  for (const rule of rules) {
355
446
  if (rule.keywords.some((kw) => lower.includes(kw))) {
@@ -362,8 +453,92 @@ export function createServer() {
362
453
  const text = matches.map((m) => `**${m.name}** — ${m.reason}`).join("\n");
363
454
  return textResponse(`Recommended playbook(s):\n\n${text}`);
364
455
  });
365
- // 14. Read the gap register
366
- server.tool("tide_gaps", "Read the gap register — what is still uncertain or unresolved in the pack", async () => {
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 () => {
367
542
  const full = join(PACK_ROOT, "GAP_REGISTER.md");
368
543
  if (!existsSync(full))
369
544
  return errorResponse("GAP_REGISTER.md not found");
@@ -389,6 +564,21 @@ export function createServer() {
389
564
  const promptContent = readPackFile("prompts", "secure-existing-app") ?? "";
390
565
  return { messages: [{ role: "user", content: { type: "text", text: promptContent } }] };
391
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
+ });
392
582
  server.prompt("tide-build-from-scenario", "Start building a Tide app from a known scenario pattern", {
393
583
  scenario: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`),
394
584
  framework: z.enum(["nextjs", "react-vite", "vanilla"]).describe("Target framework"),
@@ -411,5 +601,17 @@ export function createServer() {
411
601
  }],
412
602
  };
413
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
+ });
414
616
  return server;
415
617
  }
package/package.json CHANGED
@@ -1,45 +1,48 @@
1
- {
2
- "name": "@tideorg/mcp",
3
- "version": "1.4.2",
4
- "description": "MCP server exposing Tide operational guidance — canon, playbooks, skills, prompts, adapters, and scenarios for AI coding agents",
5
- "type": "module",
6
- "bin": {
7
- "tide-agent-pack": "mcp-server/dist/index.js"
8
- },
9
- "scripts": {
10
- "build": "cd mcp-server && npm run build",
11
- "prepublishOnly": "npm run build"
12
- },
13
- "files": [
14
- "mcp-server/dist/",
15
- "canon/",
16
- "playbooks/",
17
- "skills/",
18
- "prompts/",
19
- "adapters/",
20
- "reference-apps/",
21
- "GAP_REGISTER.md"
22
- ],
23
- "dependencies": {
24
- "@modelcontextprotocol/sdk": "^1.12.1",
25
- "zod": "^3.24.0"
26
- },
27
- "keywords": [
28
- "mcp",
29
- "tidecloak",
30
- "tide",
31
- "auth",
32
- "security",
33
- "threshold-cryptography",
34
- "ai-agent",
35
- "coding-agent"
36
- ],
37
- "license": "MIT",
38
- "repository": {
39
- "type": "git",
40
- "url": "https://github.com/tide-foundation/tide-agent-pack"
41
- },
42
- "engines": {
43
- "node": ">=18"
44
- }
45
- }
1
+ {
2
+ "name": "@tideorg/mcp",
3
+ "version": "1.9.1",
4
+ "description": "MCP server exposing Tide operational guidance — canon, playbooks, skills, prompts, adapters, and scenarios for AI coding agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "tide-agent-pack": "mcp-server/dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "cd mcp-server && npm run build",
11
+ "test": "cd mcp-server && npm test",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "files": [
15
+ "mcp-server/dist/",
16
+ "canon/",
17
+ "playbooks/",
18
+ "skills/",
19
+ "prompts/",
20
+ "adapters/",
21
+ "reference-apps/",
22
+ "GAP_REGISTER.md",
23
+ "PRIVACY.md",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.12.1",
28
+ "zod": "^3.24.0"
29
+ },
30
+ "keywords": [
31
+ "mcp",
32
+ "tidecloak",
33
+ "tide",
34
+ "auth",
35
+ "security",
36
+ "threshold-cryptography",
37
+ "ai-agent",
38
+ "coding-agent"
39
+ ],
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/tide-foundation/tide-agent-pack"
44
+ },
45
+ "engines": {
46
+ "node": ">=18"
47
+ }
48
+ }
@@ -298,7 +298,9 @@ export function ProfileButton() {
298
298
  import { useTideCloak } from '@tidecloak/nextjs';
299
299
 
300
300
  export function ProfileButton() {
301
- const { authenticated, user, login, logout } = useTideCloak();
301
+ // useTideCloak() has no `user` object. Read claims via getValueFromIdToken /
302
+ // getValueFromToken (e.g. getValueFromIdToken('preferred_username')).
303
+ const { authenticated, login, logout } = useTideCloak();
302
304
 
303
305
  if (!authenticated) {
304
306
  return <button onClick={login}>Login</button>;
@@ -334,34 +336,37 @@ export async function GET(req) {
334
336
  }
335
337
  ```
336
338
 
337
- **After** (Tide - manual JWT verification):
339
+ **After** (Tide - server-side JWT verification):
338
340
 
339
- You must implement server-side JWT verification. Tide SDK does not provide automatic API protection.
341
+ Tide ships a server-side verification helper. `@tidecloak/nextjs/server` exports `verifyTideCloakToken(config, token, allowedRoles?)` (re-exported from `@tidecloak/verify`). Use it instead of hand-rolling JWKS verification.
340
342
 
341
- See [verify-jwt-server-side.md](verify-jwt-server-side.md) for full implementation.
342
-
343
- **Quick pattern** (based on keylessh):
343
+ **Quick pattern** (supported helper):
344
344
  ```typescript
345
345
  // app/api/users/route.ts
346
346
  import { NextRequest } from 'next/server';
347
- import { verifyTideJWT } from '@/lib/auth/tideJWT'; // See verify-jwt-server-side.md
347
+ import { verifyTideCloakToken } from '@tidecloak/nextjs/server';
348
+ import tcConfig from '../../../data/tidecloak.json';
348
349
 
349
350
  export async function GET(req: NextRequest) {
350
351
  const authHeader = req.headers.get('authorization');
351
352
  if (!authHeader) {
352
353
  return Response.json({ error: 'Unauthorized' }, { status: 401 });
353
354
  }
355
+ // Accepts "Bearer <jwt>" or "DPoP <jwt>"
356
+ const token = authHeader.replace(/^(Bearer|DPoP) /, '');
354
357
 
355
- try {
356
- const jwt = await verifyTideJWT(authHeader.replace('Bearer ', ''));
357
- return Response.json({ users: [...] });
358
- } catch (err) {
358
+ // Arg order is (config, token, allowedRoles) — config FIRST.
359
+ const payload = await verifyTideCloakToken(tcConfig, token, []);
360
+ if (!payload) {
359
361
  return Response.json({ error: 'Invalid token' }, { status: 401 });
360
362
  }
363
+ return Response.json({ users: [...] });
361
364
  }
362
365
  ```
363
366
 
364
- **Critical**: You must create `lib/auth/tideJWT.ts` with local JWKS verification. See [verify-jwt-server-side.md](verify-jwt-server-side.md) for exact implementation.
367
+ `verifyTideCloakToken` verifies the signature against the adapter's embedded `jwk` (local JWKS), checks the issuer/`azp`, and optionally enforces `allowedRoles`. It returns the decoded payload on success and `false` on failure.
368
+
369
+ **If you need the manual version** (or DPoP proof re-verification): see [verify-jwt-server-side.md](verify-jwt-server-side.md), which shows the same checks hand-rolled with `jose` in `lib/auth/tideJWT.ts`.
365
370
 
366
371
  ---
367
372
 
@@ -390,13 +395,24 @@ export const config = {
390
395
  };
391
396
  ```
392
397
 
393
- **After** (Tide - client-side route guard):
398
+ **After** (Tide options):
399
+
400
+ Option A — **use the Tide-provided proxy/middleware helpers.** `@tidecloak/nextjs/server` ships `createTideCloakProxy` (Next.js 16+ `proxy.ts`, Node runtime) and `createTideCloakMiddleware` (Edge `middleware.ts`, still supported). These replace a NextAuth-style middleware export and perform real token verification via `verifyTideCloakToken`:
401
+ ```typescript
402
+ // proxy.ts (Next.js 16+) — or middleware.ts on 15 and earlier
403
+ import { createTideCloakProxy } from '@tidecloak/nextjs/server';
404
+ import tcConfig from './data/tidecloak.json';
394
405
 
395
- **Remove proxy.ts / middleware.ts** if it only checks auth. Tide provider handles client-side auth state.
406
+ export default createTideCloakProxy(tcConfig);
407
+
408
+ export const config = {
409
+ matcher: ['/dashboard/:path*', '/admin/:path*'],
410
+ };
411
+ ```
396
412
 
397
- For API protection, use server-side JWT verification (Step 8), not proxy/middleware.
413
+ Option B **remove proxy.ts / middleware.ts** and rely on the client-side provider for UX gating plus server-side JWT verification in each API route (Step 8). Do this if you do not want request-interception at all.
398
414
 
399
- **Warning**: `proxy.ts` (and legacy `middleware.ts`) route protection is UI gating only, NOT real authorization. See [canon/feature-mapping.md](../canon/feature-mapping.md#protected-routes-vs-protected-apis).
415
+ **Warning**: A plain, hand-written `proxy.ts` / `middleware.ts` that only checks cookie presence is UI gating only, NOT real authorization. The `createTideCloakProxy` / `createTideCloakMiddleware` helpers above DO verify the token, but you should still verify JWTs inside API routes (Step 8) as the authoritative enforcement point. See [canon/feature-mapping.md](../canon/feature-mapping.md#protected-routes-vs-protected-apis).
400
416
 
401
417
  If you want client-side route guards, implement in component:
402
418
  ```typescript
@@ -632,7 +648,7 @@ grep -r "from 'next-auth\|from \"next-auth" . --include="*.tsx" --include="*.ts"
632
648
 
633
649
  **Cause**: Old auth proxy or middleware still active.
634
650
 
635
- **Fix**: Remove or update `proxy.ts` (or legacy `middleware.ts`). Tide has no built-in Next.js request interception. Implement client-side route guards (Step 9) or server-side JWT verification.
651
+ **Fix**: Remove or update `proxy.ts` (or legacy `middleware.ts`). Either switch it to Tide's `createTideCloakProxy` / `createTideCloakMiddleware` from `@tidecloak/nextjs/server` (Step 9, Option A), or remove it and rely on client-side route guards plus server-side JWT verification (Step 8).
636
652
 
637
653
  ---
638
654
 
@@ -83,7 +83,7 @@ ls -la app/ 2>/dev/null && echo "App Router" || echo "Pages Router"
83
83
  npm install @tidecloak/nextjs
84
84
  ```
85
85
 
86
- **Verify version**: All `@tidecloak/*` packages are currently at `0.13.x`. Run `npm view @tidecloak/nextjs version` to confirm. Do not assume `1.0.0`.
86
+ **Verify version**: `@tidecloak/*` packages are currently at `0.13.33` (run `npm view @tidecloak/nextjs version` to confirm/pin). Do not assume `1.0.0`.
87
87
 
88
88
  **Next.js 16+ bundler**: Next.js 16 defaults to Turbopack. The Tide SDK requires webpack for `strictExportPresence = false` and `@tidecloak/react` ESM alias. Update `package.json` scripts:
89
89
  ```json
@@ -137,13 +137,18 @@ Tide E2EE uses tag-based roles for **self-encryption** (user-bound, private data
137
137
  // Client-side E2EE with role enforcement (self-encryption)
138
138
  const { doEncrypt, doDecrypt } = useTideCloak();
139
139
 
140
+ // Both take a single argument: an ARRAY of items. Each encrypt item is
141
+ // { data, tags }; the tags drive the _tide_<tag>.self* role check.
140
142
  // Requires _tide_ssn.selfencrypt role
141
- const ciphertext = await doEncrypt('ssn', '123-45-6789');
143
+ const [ciphertext] = await doEncrypt([{ data: '123-45-6789', tags: ['ssn'] }]);
142
144
 
145
+ // doDecrypt takes the array of encrypted items ({ encrypted, tags }).
143
146
  // Requires _tide_ssn.selfdecrypt role
144
- const plaintext = await doDecrypt('ssn', ciphertext);
147
+ const [plaintext] = await doDecrypt([{ encrypted: ciphertext, tags: ['ssn'] }]);
145
148
  ```
146
149
 
150
+ **Signature note**: The `useTideCloak()` `doEncrypt`/`doDecrypt` are single-argument (`data`) wrappers. `data` is an array so you can encrypt/decrypt multiple fields in one call. This matches the shapes in [setup-forseti-e2ee.md](setup-forseti-e2ee.md) and [configure-e2ee-roles-and-policies.md](configure-e2ee-roles-and-policies.md). Do NOT call them with positional `(tag, value)` arguments — that form does not exist.
151
+
147
152
  Fabric enforces roles cryptographically. See [canon/concepts.md#tag-based-e2ee-roles](../canon/concepts.md#tag-based-e2ee-roles).
148
153
 
149
154
  **Self-encryption is user-bound**: only the encrypting user can decrypt. Giving another user the `selfdecrypt` role does NOT let them decrypt your data. For shared data between users, use policy-governed VVK encryption with a Forseti contract instead. See [setup-forseti-e2ee.md](setup-forseti-e2ee.md) and [canon/anti-patterns.md AP-24](../canon/anti-patterns.md#ap-24-using-self-encryption-for-shared-data-between-users).