@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.
- package/CHANGELOG.md +88 -0
- package/GAP_REGISTER.md +18 -8
- package/PRIVACY.md +41 -0
- package/README.md +207 -197
- package/adapters/AGENTS.md +3 -2
- package/adapters/CLAUDE.md +1 -1
- package/canon/anti-patterns.md +55 -62
- package/canon/concepts.md +46 -34
- package/canon/custom-contracts.md +12 -8
- package/canon/feature-mapping.md +27 -75
- package/canon/framework-matrix.md +69 -22
- package/canon/hosting-options.md +111 -0
- package/canon/iga-change-requests-api.md +211 -0
- package/canon/invariants.md +33 -35
- package/canon/security-gap-mapping.md +506 -0
- package/canon/security-runtime-probes.md +177 -0
- package/canon/tidecloak-bootstrap.md +21 -16
- package/canon/tidecloak-endpoints.md +60 -98
- package/canon/troubleshooting.md +115 -53
- package/canon/version-policy.md +8 -12
- package/mcp-server/dist/server.js +221 -19
- package/package.json +48 -45
- package/playbooks/add-auth-nextjs-existing.md +33 -17
- package/playbooks/add-auth-nextjs-fresh.md +1 -1
- package/playbooks/add-rbac-nextjs.md +7 -2
- package/playbooks/bootstrap-realm-from-template.md +33 -18
- package/playbooks/deploy-tidecloak-docker.md +31 -28
- package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
- package/playbooks/initialize-admin-and-link-account.md +22 -19
- package/playbooks/protect-api-nextjs.md +40 -1
- package/playbooks/protect-aspnet-core-asgard.md +313 -0
- package/playbooks/protect-routes-nextjs.md +24 -10
- package/playbooks/provision-tidecloak-skycloak.md +213 -0
- package/playbooks/setup-forseti-e2ee.md +32 -50
- package/playbooks/setup-iga-admin-panel.md +113 -171
- package/playbooks/verify-jwt-server-side.md +20 -1
- package/prompts/build-private-customer-portal.md +1 -1
- package/prompts/security-gap-analysis.md +55 -0
- package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
- package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
- package/reference-apps/encrypted-communication/scenario.md +2 -2
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
- package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
- package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
- package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
- package/reference-apps/iga-admin-governance/scenario.md +15 -13
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
- package/skills/tide-diagnostics/SKILL.md +1 -1
- package/skills/tide-integration/SKILL.md +9 -18
- package/skills/tide-mcp-qa/SKILL.md +139 -0
- package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
- package/skills/tide-reviewer/SKILL.md +1 -1
- package/skills/tide-security-analyst/SKILL.md +182 -0
- package/skills/tide-setup/SKILL.md +1 -1
- package/canon/delegation.md +0 -195
- 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.
|
|
165
|
+
version: "1.9.1",
|
|
166
166
|
});
|
|
167
167
|
// 1. List available content
|
|
168
|
-
server.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
366
|
-
server.
|
|
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
|
-
"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
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
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
|
-
|
|
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 -
|
|
339
|
+
**After** (Tide - server-side JWT verification):
|
|
338
340
|
|
|
339
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
406
|
+
export default createTideCloakProxy(tcConfig);
|
|
407
|
+
|
|
408
|
+
export const config = {
|
|
409
|
+
matcher: ['/dashboard/:path*', '/admin/:path*'],
|
|
410
|
+
};
|
|
411
|
+
```
|
|
396
412
|
|
|
397
|
-
|
|
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`
|
|
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
|
|
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**:
|
|
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(
|
|
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'
|
|
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).
|