@tideorg/mcp 1.4.1 → 1.4.2
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/GAP_REGISTER.md +1 -1
- package/README.md +80 -3
- package/adapters/AGENTS.md +0 -0
- package/adapters/CLAUDE.md +0 -0
- package/adapters/replit.md +0 -0
- package/canon/anti-patterns.md +0 -0
- package/canon/concepts.md +0 -0
- package/canon/custom-contracts.md +0 -0
- package/canon/delegation.md +0 -0
- package/canon/feature-mapping.md +0 -0
- package/canon/framework-matrix.md +0 -0
- package/canon/invariants.md +0 -0
- package/canon/redirect-handler.md +0 -0
- package/canon/tidecloak-bootstrap.md +64 -0
- package/canon/tidecloak-endpoints.md +0 -0
- package/canon/troubleshooting.md +86 -0
- package/canon/version-policy.md +0 -0
- package/mcp-server/dist/http.d.ts +2 -0
- package/mcp-server/dist/http.js +46 -0
- package/mcp-server/dist/index.d.ts +0 -0
- package/mcp-server/dist/index.js +2 -601
- package/mcp-server/dist/server.d.ts +2 -0
- package/mcp-server/dist/server.js +415 -0
- package/package.json +1 -1
- package/playbooks/add-auth-nextjs-existing.md +0 -0
- package/playbooks/add-auth-nextjs-fresh.md +0 -0
- package/playbooks/add-rbac-nextjs.md +0 -0
- package/playbooks/bootstrap-realm-from-template.md +0 -0
- package/playbooks/configure-e2ee-roles-and-policies.md +0 -0
- package/playbooks/deploy-tidecloak-docker.md +0 -0
- package/playbooks/diagnose-broken-login.md +0 -0
- package/playbooks/diagnose-missing-roles-or-claims.md +0 -0
- package/playbooks/initialize-admin-and-link-account.md +0 -0
- package/playbooks/migrate-from-existing-auth.md +0 -0
- package/playbooks/protect-api-nextjs.md +0 -0
- package/playbooks/protect-routes-nextjs.md +0 -0
- package/playbooks/setup-forseti-e2ee.md +0 -0
- package/playbooks/setup-iga-admin-panel.md +0 -0
- package/playbooks/setup-server-delegation.md +0 -0
- package/playbooks/start-tidecloak-dev.md +0 -0
- package/playbooks/verify-jwt-server-side.md +0 -0
- package/prompts/add-admin-approval-flow.md +0 -0
- package/prompts/build-private-customer-portal.md +0 -0
- package/prompts/migrate-generic-auth-to-tide.md +0 -0
- package/prompts/secure-existing-app.md +0 -0
- package/reference-apps/INDEX.md +0 -0
- package/reference-apps/encrypted-communication/anti-patterns.md +0 -0
- package/reference-apps/encrypted-communication/bootstrap-sequence.md +0 -0
- package/reference-apps/encrypted-communication/manifest.yaml +0 -0
- package/reference-apps/encrypted-communication/role-policy-matrix.md +0 -0
- package/reference-apps/encrypted-communication/scenario.md +0 -0
- package/reference-apps/git-pr-signing-service/anti-patterns.md +0 -0
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +0 -0
- package/reference-apps/git-pr-signing-service/manifest.yaml +0 -0
- package/reference-apps/git-pr-signing-service/role-policy-matrix.md +0 -0
- package/reference-apps/git-pr-signing-service/scenario.md +0 -0
- package/reference-apps/iga-admin-governance/anti-patterns.md +0 -0
- package/reference-apps/iga-admin-governance/bootstrap-sequence.md +0 -0
- package/reference-apps/iga-admin-governance/manifest.yaml +0 -0
- package/reference-apps/iga-admin-governance/role-policy-matrix.md +0 -0
- package/reference-apps/iga-admin-governance/scenario.md +0 -0
- package/reference-apps/organisation-password-manager/anti-patterns.md +0 -0
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +0 -0
- package/reference-apps/organisation-password-manager/manifest.yaml +0 -0
- package/reference-apps/organisation-password-manager/role-policy-matrix.md +0 -0
- package/reference-apps/organisation-password-manager/scenario.md +0 -0
- package/reference-apps/policy-governed-signing/anti-patterns.md +0 -0
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +0 -0
- package/reference-apps/policy-governed-signing/manifest.yaml +0 -0
- package/reference-apps/policy-governed-signing/role-policy-matrix.md +0 -0
- package/reference-apps/policy-governed-signing/scenario.md +0 -0
- package/skills/tide-diagnostics/SKILL.md +0 -0
- package/skills/tide-integration/SKILL.md +0 -0
- package/skills/tide-learning-capture/SKILL.md +0 -0
- package/skills/tide-rbac-and-e2ee/SKILL.md +0 -0
- package/skills/tide-reviewer/SKILL.md +0 -0
- package/skills/tide-route-and-api-protection/SKILL.md +0 -0
- package/skills/tide-scenario-resolver/SKILL.md +0 -0
- package/skills/tide-setup/SKILL.md +0 -0
- package/skills/tide-solutions-architect/SKILL.md +0 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
|
+
// Resolve pack root.
|
|
6
|
+
// - TIDE_PACK_ROOT env var takes priority (custom deployments)
|
|
7
|
+
// - Default: two levels up from dist/ → repo/package root
|
|
8
|
+
const PACK_ROOT = process.env.TIDE_PACK_ROOT
|
|
9
|
+
? resolve(process.env.TIDE_PACK_ROOT)
|
|
10
|
+
: resolve(import.meta.dirname, "..", "..");
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
function listMarkdownFiles(dir) {
|
|
15
|
+
const full = join(PACK_ROOT, dir);
|
|
16
|
+
if (!existsSync(full))
|
|
17
|
+
return [];
|
|
18
|
+
return readdirSync(full)
|
|
19
|
+
.filter((f) => f.endsWith(".md"))
|
|
20
|
+
.map((f) => f.replace(/\.md$/, ""));
|
|
21
|
+
}
|
|
22
|
+
function listDirectories(dir) {
|
|
23
|
+
const full = join(PACK_ROOT, dir);
|
|
24
|
+
if (!existsSync(full))
|
|
25
|
+
return [];
|
|
26
|
+
return readdirSync(full, { withFileTypes: true })
|
|
27
|
+
.filter((d) => d.isDirectory())
|
|
28
|
+
.map((d) => d.name);
|
|
29
|
+
}
|
|
30
|
+
function readPackFile(dir, name) {
|
|
31
|
+
const file = name.endsWith(".md") ? name : `${name}.md`;
|
|
32
|
+
const full = join(PACK_ROOT, dir, file);
|
|
33
|
+
if (!existsSync(full))
|
|
34
|
+
return null;
|
|
35
|
+
return readFileSync(full, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
function readSkill(name) {
|
|
38
|
+
const full = join(PACK_ROOT, "skills", name, "SKILL.md");
|
|
39
|
+
if (!existsSync(full))
|
|
40
|
+
return null;
|
|
41
|
+
return readFileSync(full, "utf-8");
|
|
42
|
+
}
|
|
43
|
+
function scenarioExists(scenario) {
|
|
44
|
+
return existsSync(join(PACK_ROOT, "reference-apps", scenario));
|
|
45
|
+
}
|
|
46
|
+
function readScenarioFile(scenario, fileName) {
|
|
47
|
+
const full = join(PACK_ROOT, "reference-apps", scenario, fileName);
|
|
48
|
+
if (!existsSync(full))
|
|
49
|
+
return null;
|
|
50
|
+
return readFileSync(full, "utf-8");
|
|
51
|
+
}
|
|
52
|
+
function scenarioKeywordsFromName(name) {
|
|
53
|
+
return name
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.split(/[-_\s]+/)
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
function parseScenarioManifest(raw) {
|
|
59
|
+
if (!raw)
|
|
60
|
+
return {};
|
|
61
|
+
const result = {};
|
|
62
|
+
const lines = raw.split("\n");
|
|
63
|
+
let currentListKey = null;
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
67
|
+
continue;
|
|
68
|
+
const keyMatch = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(trimmed);
|
|
69
|
+
if (keyMatch) {
|
|
70
|
+
const [, key, value] = keyMatch;
|
|
71
|
+
if (value === "") {
|
|
72
|
+
currentListKey = key;
|
|
73
|
+
result[key] = [];
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
currentListKey = null;
|
|
77
|
+
result[key] = value.replace(/^["']|["']$/g, "");
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (currentListKey && trimmed.startsWith("- ")) {
|
|
82
|
+
const arr = result[currentListKey] ?? [];
|
|
83
|
+
arr.push(trimmed.replace(/^- /, "").trim());
|
|
84
|
+
result[currentListKey] = arr;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
function textResponse(text) {
|
|
90
|
+
return { content: [{ type: "text", text }] };
|
|
91
|
+
}
|
|
92
|
+
function errorResponse(text) {
|
|
93
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
94
|
+
}
|
|
95
|
+
function formatList(title, items) {
|
|
96
|
+
return `## ${title}\n${items.length ? items.map((i) => `- ${i}`).join("\n") : "- (none)"}`;
|
|
97
|
+
}
|
|
98
|
+
function getScenarioSummary(scenario) {
|
|
99
|
+
const manifest = parseScenarioManifest(readScenarioFile(scenario, "manifest.yaml"));
|
|
100
|
+
const title = typeof manifest.title === "string" ? manifest.title : scenario;
|
|
101
|
+
const category = typeof manifest.category === "string" ? manifest.category : "uncategorized";
|
|
102
|
+
const corePatterns = Array.isArray(manifest.core_patterns) ? manifest.core_patterns : [];
|
|
103
|
+
const defaultPlaybooks = Array.isArray(manifest.default_playbooks) ? manifest.default_playbooks : [];
|
|
104
|
+
const matchKeywords = Array.isArray(manifest.match_keywords) ? manifest.match_keywords : [];
|
|
105
|
+
return {
|
|
106
|
+
scenario,
|
|
107
|
+
title,
|
|
108
|
+
category,
|
|
109
|
+
corePatterns,
|
|
110
|
+
defaultPlaybooks,
|
|
111
|
+
matchKeywords,
|
|
112
|
+
manifest,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Negative signals: if the situation contains these words and the scenario
|
|
116
|
+
// is not the right match, suppress the score.
|
|
117
|
+
const SCENARIO_NEGATIVE_SIGNALS = {
|
|
118
|
+
"policy-governed-signing": ["ssh", "sign", "signing", "document", "transaction", "certificate"],
|
|
119
|
+
"git-pr-signing-service": ["git", "commit", "merge", "pr", "pull request", "verified", "github"],
|
|
120
|
+
};
|
|
121
|
+
function scoreScenarioMatch(scenario, situation) {
|
|
122
|
+
const lower = situation.toLowerCase();
|
|
123
|
+
const summary = getScenarioSummary(scenario);
|
|
124
|
+
const manifestKeywords = [
|
|
125
|
+
scenario,
|
|
126
|
+
...scenarioKeywordsFromName(scenario),
|
|
127
|
+
...summary.corePatterns.map((p) => String(p).toLowerCase()),
|
|
128
|
+
...summary.title.toLowerCase().split(/\s+/),
|
|
129
|
+
...summary.category.toLowerCase().split(/\s+/),
|
|
130
|
+
];
|
|
131
|
+
const unique = Array.from(new Set(manifestKeywords.filter(Boolean)));
|
|
132
|
+
let score = unique.reduce((acc, kw) => acc + (lower.includes(kw) ? 1 : 0), 0);
|
|
133
|
+
for (const phrase of summary.matchKeywords) {
|
|
134
|
+
if (lower.includes(String(phrase).toLowerCase())) {
|
|
135
|
+
score += 3;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const requiredSignals = SCENARIO_NEGATIVE_SIGNALS[scenario];
|
|
139
|
+
if (requiredSignals && score > 0) {
|
|
140
|
+
const hasAnySignal = requiredSignals.some((sig) => lower.includes(sig));
|
|
141
|
+
if (!hasAnySignal) {
|
|
142
|
+
score = 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
...summary,
|
|
147
|
+
score,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Content catalogs
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
const CANON_FILES = listMarkdownFiles("canon");
|
|
154
|
+
const PLAYBOOK_FILES = listMarkdownFiles("playbooks");
|
|
155
|
+
const PROMPT_FILES = listMarkdownFiles("prompts");
|
|
156
|
+
const ADAPTER_FILES = listMarkdownFiles("adapters");
|
|
157
|
+
const SKILL_DIRS = listDirectories("skills");
|
|
158
|
+
const REFERENCE_APP_DIRS = listDirectories("reference-apps");
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Server factory
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
export function createServer() {
|
|
163
|
+
const server = new McpServer({
|
|
164
|
+
name: "@tideorg/mcp",
|
|
165
|
+
version: "1.4.1",
|
|
166
|
+
});
|
|
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"),
|
|
172
|
+
}, async ({ category }) => {
|
|
173
|
+
const sections = {};
|
|
174
|
+
if (category === "all" || category === "canon")
|
|
175
|
+
sections.canon = CANON_FILES;
|
|
176
|
+
if (category === "all" || category === "playbooks")
|
|
177
|
+
sections.playbooks = PLAYBOOK_FILES;
|
|
178
|
+
if (category === "all" || category === "skills")
|
|
179
|
+
sections.skills = SKILL_DIRS;
|
|
180
|
+
if (category === "all" || category === "prompts")
|
|
181
|
+
sections.prompts = PROMPT_FILES;
|
|
182
|
+
if (category === "all" || category === "adapters")
|
|
183
|
+
sections.adapters = ADAPTER_FILES;
|
|
184
|
+
if (category === "all" || category === "scenarios")
|
|
185
|
+
sections.scenarios = REFERENCE_APP_DIRS;
|
|
186
|
+
const text = Object.entries(sections)
|
|
187
|
+
.map(([cat, items]) => formatList(cat, items))
|
|
188
|
+
.join("\n\n");
|
|
189
|
+
return textResponse(text);
|
|
190
|
+
});
|
|
191
|
+
// 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 }) => {
|
|
193
|
+
const content = readPackFile("canon", name);
|
|
194
|
+
if (!content)
|
|
195
|
+
return errorResponse(`Canon file '${name}' not found. Available: ${CANON_FILES.join(", ")}`);
|
|
196
|
+
return textResponse(content);
|
|
197
|
+
});
|
|
198
|
+
// 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 }) => {
|
|
200
|
+
const content = readPackFile("playbooks", name);
|
|
201
|
+
if (!content)
|
|
202
|
+
return errorResponse(`Playbook '${name}' not found. Available: ${PLAYBOOK_FILES.join(", ")}`);
|
|
203
|
+
return textResponse(content);
|
|
204
|
+
});
|
|
205
|
+
// 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 }) => {
|
|
207
|
+
const content = readSkill(name);
|
|
208
|
+
if (!content)
|
|
209
|
+
return errorResponse(`Skill '${name}' not found. Available: ${SKILL_DIRS.join(", ")}`);
|
|
210
|
+
return textResponse(content);
|
|
211
|
+
});
|
|
212
|
+
// 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 }) => {
|
|
214
|
+
const content = readPackFile("prompts", name);
|
|
215
|
+
if (!content)
|
|
216
|
+
return errorResponse(`Prompt '${name}' not found. Available: ${PROMPT_FILES.join(", ")}`);
|
|
217
|
+
return textResponse(content);
|
|
218
|
+
});
|
|
219
|
+
// 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 }) => {
|
|
221
|
+
const content = readPackFile("adapters", name);
|
|
222
|
+
if (!content)
|
|
223
|
+
return errorResponse(`Adapter '${name}' not found. Available: ${ADAPTER_FILES.join(", ")}`);
|
|
224
|
+
return textResponse(content);
|
|
225
|
+
});
|
|
226
|
+
// 7. List available scenarios
|
|
227
|
+
server.tool("tide_list_scenarios", "List all available scenario patterns under reference-apps/", async () => {
|
|
228
|
+
if (REFERENCE_APP_DIRS.length === 0)
|
|
229
|
+
return textResponse("No scenarios found under reference-apps/");
|
|
230
|
+
const lines = REFERENCE_APP_DIRS.map((name) => {
|
|
231
|
+
const summary = getScenarioSummary(name);
|
|
232
|
+
return `- ${summary.scenario} — ${summary.title} [${summary.category}]`;
|
|
233
|
+
});
|
|
234
|
+
return textResponse(`Available scenarios:\n\n${lines.join("\n")}`);
|
|
235
|
+
});
|
|
236
|
+
// 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 }) => {
|
|
238
|
+
if (!scenarioExists(name))
|
|
239
|
+
return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
|
|
240
|
+
const scenarioContent = readScenarioFile(name, "scenario.md");
|
|
241
|
+
if (!scenarioContent)
|
|
242
|
+
return errorResponse(`Scenario '${name}' exists but scenario.md is missing.`);
|
|
243
|
+
const antiPatterns = readScenarioFile(name, "anti-patterns.md");
|
|
244
|
+
const text = antiPatterns ? `${scenarioContent}\n\n---\n\n## Anti-patterns\n\n${antiPatterns}` : scenarioContent;
|
|
245
|
+
return textResponse(text);
|
|
246
|
+
});
|
|
247
|
+
// 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 }) => {
|
|
249
|
+
if (!scenarioExists(name))
|
|
250
|
+
return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
|
|
251
|
+
const content = readScenarioFile(name, "manifest.yaml");
|
|
252
|
+
if (!content)
|
|
253
|
+
return errorResponse(`Scenario '${name}' exists but manifest.yaml is missing.`);
|
|
254
|
+
return textResponse(content);
|
|
255
|
+
});
|
|
256
|
+
// 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 }) => {
|
|
258
|
+
if (!scenarioExists(name))
|
|
259
|
+
return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
|
|
260
|
+
const content = readScenarioFile(name, "role-policy-matrix.md");
|
|
261
|
+
if (!content)
|
|
262
|
+
return errorResponse(`Scenario '${name}' exists but role-policy-matrix.md is missing.`);
|
|
263
|
+
return textResponse(content);
|
|
264
|
+
});
|
|
265
|
+
// 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 }) => {
|
|
267
|
+
if (!scenarioExists(name))
|
|
268
|
+
return errorResponse(`Scenario '${name}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
|
|
269
|
+
const content = readScenarioFile(name, "bootstrap-sequence.md");
|
|
270
|
+
if (!content)
|
|
271
|
+
return errorResponse(`Scenario '${name}' exists but bootstrap-sequence.md is missing.`);
|
|
272
|
+
return textResponse(content);
|
|
273
|
+
});
|
|
274
|
+
// 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 }) => {
|
|
276
|
+
const matches = REFERENCE_APP_DIRS
|
|
277
|
+
.map((scenario) => scoreScenarioMatch(scenario, situation))
|
|
278
|
+
.filter((m) => m.score > 0)
|
|
279
|
+
.sort((a, b) => b.score - a.score);
|
|
280
|
+
if (matches.length === 0) {
|
|
281
|
+
return textResponse(`No scenario match for "${situation}". Available scenarios:\n${REFERENCE_APP_DIRS.map((s) => `- ${s}`).join("\n")}`);
|
|
282
|
+
}
|
|
283
|
+
const best = matches[0];
|
|
284
|
+
const second = matches.length > 1 ? matches[1] : null;
|
|
285
|
+
const autoSelect = best.score >= 6 && (!second || best.score - second.score >= 3);
|
|
286
|
+
const closeMatches = autoSelect ? [best] : matches.filter((m) => m.score >= best.score * 0.6 && m.score > 0);
|
|
287
|
+
if (closeMatches.length > 1) {
|
|
288
|
+
const lines = closeMatches.map((m) => {
|
|
289
|
+
const dq = typeof m.manifest.discriminating_question === "string" ? m.manifest.discriminating_question : "";
|
|
290
|
+
return `- **${m.scenario}** (${m.title}, score ${m.score})${dq ? `\n Disambiguate: ${dq}` : ""}`;
|
|
291
|
+
});
|
|
292
|
+
return textResponse([
|
|
293
|
+
`Multiple scenarios match "${situation}" (I-17 — resolve before proceeding):`,
|
|
294
|
+
"", ...lines, "",
|
|
295
|
+
`Resolve the ambiguity before selecting a playbook path.`,
|
|
296
|
+
].join("\n"));
|
|
297
|
+
}
|
|
298
|
+
const defaultPlaybooks = best.defaultPlaybooks;
|
|
299
|
+
return textResponse([
|
|
300
|
+
`Best scenario match: ${best.scenario}`,
|
|
301
|
+
`Title: ${best.title}`,
|
|
302
|
+
`Category: ${best.category}`,
|
|
303
|
+
defaultPlaybooks.length
|
|
304
|
+
? `Default playbooks:\n${defaultPlaybooks.map((p) => `- ${p}`).join("\n")}`
|
|
305
|
+
: `Default playbooks: not declared in manifest.yaml`,
|
|
306
|
+
`Use tide_scenario_manifest, tide_scenario_roles, and tide_scenario_bootstrap for scenario-specific details.`,
|
|
307
|
+
].join("\n\n"));
|
|
308
|
+
});
|
|
309
|
+
// 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 }) => {
|
|
311
|
+
const lower = situation.toLowerCase();
|
|
312
|
+
const scenarioMatches = REFERENCE_APP_DIRS
|
|
313
|
+
.map((scenario) => scoreScenarioMatch(scenario, situation))
|
|
314
|
+
.filter((m) => m.score > 0)
|
|
315
|
+
.sort((a, b) => b.score - a.score);
|
|
316
|
+
if (scenarioMatches.length > 0) {
|
|
317
|
+
const best = scenarioMatches[0];
|
|
318
|
+
const closeMatches = scenarioMatches.filter((m) => m.score >= best.score * 0.6 && m.score > 0);
|
|
319
|
+
if (closeMatches.length > 1) {
|
|
320
|
+
const lines = closeMatches.map((m) => {
|
|
321
|
+
const dq = typeof m.manifest.discriminating_question === "string" ? m.manifest.discriminating_question : "";
|
|
322
|
+
return `- **${m.scenario}** (score ${m.score})${dq ? ` — ${dq}` : ""}`;
|
|
323
|
+
});
|
|
324
|
+
return textResponse([
|
|
325
|
+
`Multiple scenarios match (I-17 — resolve before proceeding):`,
|
|
326
|
+
...lines, ``, `Resolve the ambiguity before selecting a playbook path.`,
|
|
327
|
+
].join("\n"));
|
|
328
|
+
}
|
|
329
|
+
const defaultPlaybooks = best.defaultPlaybooks;
|
|
330
|
+
return textResponse([
|
|
331
|
+
`Scenario match: ${best.scenario}`,
|
|
332
|
+
defaultPlaybooks.length
|
|
333
|
+
? `Recommended playbook sequence:\n${defaultPlaybooks.map((p) => `- ${p}`).join("\n")}`
|
|
334
|
+
: `Scenario matched, but manifest.yaml does not declare default_playbooks.`,
|
|
335
|
+
`Use tide_scenario_manifest, tide_scenario_roles, and tide_scenario_bootstrap for scenario-specific details.`,
|
|
336
|
+
].join("\n\n"));
|
|
337
|
+
}
|
|
338
|
+
const matches = [];
|
|
339
|
+
const rules = [
|
|
340
|
+
{ keywords: ["new", "fresh", "setup", "add login", "add auth", "from scratch"], name: "add-auth-nextjs-fresh", reason: "New app needs Tide auth from scratch" },
|
|
341
|
+
{ keywords: ["existing", "retrofit", "already has", "migrate"], name: "add-auth-nextjs-existing", reason: "Existing app needs Tide added" },
|
|
342
|
+
{ keywords: ["route", "page guard", "redirect", "protect page"], name: "protect-routes-nextjs", reason: "Client-side route protection (UI gating)" },
|
|
343
|
+
{ keywords: ["api", "endpoint", "server", "protect api", "backend"], name: "protect-api-nextjs", reason: "Server-side API protection" },
|
|
344
|
+
{ keywords: ["jwt", "dpop", "verify", "token"], name: "verify-jwt-server-side", reason: "Complete JWT + DPoP verification" },
|
|
345
|
+
{ keywords: ["rbac", "role", "permission", "admin", "access control"], name: "add-rbac-nextjs", reason: "Role-based access control" },
|
|
346
|
+
{ keywords: ["login broken", "hang", "stuck", "blank", "csp"], name: "diagnose-broken-login", reason: "Login diagnostics" },
|
|
347
|
+
{ keywords: ["role missing", "claim", "no role", "token empty"], name: "diagnose-missing-roles-or-claims", reason: "Missing roles/claims diagnostics" },
|
|
348
|
+
{ keywords: ["deploy", "docker", "container", "tidecloak"], name: "deploy-tidecloak-docker", reason: "Deploy TideCloak instance" },
|
|
349
|
+
{ keywords: ["e2ee", "encrypt", "decrypt", "forseti", "vault", "share", "sharing", "shared"], name: "setup-forseti-e2ee", reason: "End-to-end encryption setup" },
|
|
350
|
+
{ keywords: ["iga", "approval", "governance", "admin panel"], name: "setup-iga-admin-panel", reason: "IGA admin panel setup" },
|
|
351
|
+
{ keywords: ["bootstrap", "realm", "init", "initialize"], name: "bootstrap-realm-from-template", reason: "Bootstrap realm from template" },
|
|
352
|
+
{ keywords: ["start", "run tidecloak", "launch"], name: "start-tidecloak-dev", reason: "Start TideCloak dev instance" },
|
|
353
|
+
];
|
|
354
|
+
for (const rule of rules) {
|
|
355
|
+
if (rule.keywords.some((kw) => lower.includes(kw))) {
|
|
356
|
+
matches.push({ name: rule.name, reason: rule.reason });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (matches.length === 0) {
|
|
360
|
+
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`);
|
|
361
|
+
}
|
|
362
|
+
const text = matches.map((m) => `**${m.name}** — ${m.reason}`).join("\n");
|
|
363
|
+
return textResponse(`Recommended playbook(s):\n\n${text}`);
|
|
364
|
+
});
|
|
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 () => {
|
|
367
|
+
const full = join(PACK_ROOT, "GAP_REGISTER.md");
|
|
368
|
+
if (!existsSync(full))
|
|
369
|
+
return errorResponse("GAP_REGISTER.md not found");
|
|
370
|
+
return textResponse(readFileSync(full, "utf-8"));
|
|
371
|
+
});
|
|
372
|
+
// -------------------------------------------------------------------------
|
|
373
|
+
// Prompts
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
server.prompt("tide-build-app", "Start building a Tide-protected app from scratch", { framework: z.enum(["nextjs", "react-vite", "vanilla"]).describe("Target framework") }, async ({ framework }) => {
|
|
376
|
+
const adapterContent = readPackFile("adapters", "AGENTS") ?? "";
|
|
377
|
+
const invariantsContent = readPackFile("canon", "invariants") ?? "";
|
|
378
|
+
return {
|
|
379
|
+
messages: [{
|
|
380
|
+
role: "user",
|
|
381
|
+
content: {
|
|
382
|
+
type: "text",
|
|
383
|
+
text: `I want to build a new ${framework} app with Tide authentication, authorization, and encryption.\n\nHere are the operational instructions you must follow:\n\n${adapterContent}\n\n---\n\nHere are the security invariants you must never violate:\n\n${invariantsContent}\n\nIf the request matches a known app pattern, start with tide_choose_scenario. Otherwise use tide_choose_playbook. Then follow the resulting bootstrap and playbook path step by step.`,
|
|
384
|
+
},
|
|
385
|
+
}],
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
server.prompt("tide-secure-existing", "Add Tide to an existing app", async () => {
|
|
389
|
+
const promptContent = readPackFile("prompts", "secure-existing-app") ?? "";
|
|
390
|
+
return { messages: [{ role: "user", content: { type: "text", text: promptContent } }] };
|
|
391
|
+
});
|
|
392
|
+
server.prompt("tide-build-from-scenario", "Start building a Tide app from a known scenario pattern", {
|
|
393
|
+
scenario: z.string().describe(`Scenario name. Available: ${REFERENCE_APP_DIRS.join(", ")}`),
|
|
394
|
+
framework: z.enum(["nextjs", "react-vite", "vanilla"]).describe("Target framework"),
|
|
395
|
+
}, async ({ scenario, framework }) => {
|
|
396
|
+
if (!scenarioExists(scenario))
|
|
397
|
+
throw new Error(`Scenario '${scenario}' not found. Available: ${REFERENCE_APP_DIRS.join(", ")}`);
|
|
398
|
+
const adapterContent = readPackFile("adapters", "AGENTS") ?? "";
|
|
399
|
+
const invariantsContent = readPackFile("canon", "invariants") ?? "";
|
|
400
|
+
const scenarioContent = readScenarioFile(scenario, "scenario.md") ?? "";
|
|
401
|
+
const manifestContent = readScenarioFile(scenario, "manifest.yaml") ?? "";
|
|
402
|
+
const rolesContent = readScenarioFile(scenario, "role-policy-matrix.md") ?? "";
|
|
403
|
+
const bootstrapContent = readScenarioFile(scenario, "bootstrap-sequence.md") ?? "";
|
|
404
|
+
return {
|
|
405
|
+
messages: [{
|
|
406
|
+
role: "user",
|
|
407
|
+
content: {
|
|
408
|
+
type: "text",
|
|
409
|
+
text: `I want to build a ${framework} Tide app using the scenario pattern '${scenario}'.\n\nFollow these adapter instructions first:\n\n${adapterContent}\n\n---\n\nSecurity invariants:\n\n${invariantsContent}\n\n---\n\nScenario summary:\n\n${scenarioContent}\n\n---\n\nScenario manifest:\n\n${manifestContent}\n\n---\n\nRole and policy matrix:\n\n${rolesContent}\n\n---\n\nBootstrap sequence:\n\n${bootstrapContent}\n\nStart by honoring the scenario bootstrap and role/policy requirements before falling back to generic playbook selection.`,
|
|
410
|
+
},
|
|
411
|
+
}],
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
return server;
|
|
415
|
+
}
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/reference-apps/INDEX.md
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|