@ductape/mcp 0.3.4 → 0.3.6

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.
@@ -4,6 +4,7 @@ interface AnalyticsDecision {
4
4
  rationale?: string;
5
5
  autoCapture?: 'disabled' | 'reviewed';
6
6
  privacyReviewed?: boolean;
7
+ settingsContractsReviewed?: boolean;
7
8
  }
8
9
  export interface FrontendAnalyticsValidationResult {
9
10
  valid: boolean;
@@ -18,6 +19,7 @@ export interface FrontendAnalyticsValidationResult {
18
19
  framework: 'react' | 'vue' | 'client';
19
20
  decision?: AnalyticsDecision;
20
21
  signals: Record<string, boolean>;
22
+ evidence: Record<string, string[]>;
21
23
  errors: string[];
22
24
  warnings: string[];
23
25
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"frontend-analytics-validator.d.ts","sourceRoot":"","sources":["../src/frontend-analytics-validator.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,CAAC;AAE5E,UAAU,iBAAiB;IACzB,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACtC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAMD,MAAM,WAAW,iCAAiC;IAChD,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,YAAY,EAAE,KAAK,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;QACtC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC,CAAC;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AA0DD,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,MAAM,GACjB,iCAAiC,CAkGnC"}
1
+ {"version":3,"file":"frontend-analytics-validator.d.ts","sourceRoot":"","sources":["../src/frontend-analytics-validator.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,CAAC;AAE5E,UAAU,iBAAiB;IACzB,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACtC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAMD,MAAM,WAAW,iCAAiC;IAChD,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,YAAY,EAAE,KAAK,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;QACtC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACnC,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC,CAAC;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AA0DD,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,MAAM,GACjB,iCAAiC,CAyInC"}
@@ -47,17 +47,18 @@ function packageFramework(manifest) {
47
47
  return 'client';
48
48
  return undefined;
49
49
  }
50
- function scanSource(applicationRoot) {
50
+ function scanSource(projectRoot, roots) {
51
51
  const chunks = [];
52
- walk(applicationRoot, (file) => {
53
- if (!SOURCE_EXTENSIONS.has(path.extname(file)))
54
- return;
55
- const stats = fs.statSync(file);
56
- if (stats.size > MAX_SOURCE_BYTES)
57
- return;
58
- chunks.push(fs.readFileSync(file, 'utf8'));
59
- });
60
- return chunks.join('\n');
52
+ for (const sourceRoot of roots)
53
+ walk(sourceRoot, (file) => {
54
+ if (!SOURCE_EXTENSIONS.has(path.extname(file)))
55
+ return;
56
+ const stats = fs.statSync(file);
57
+ if (stats.size > MAX_SOURCE_BYTES)
58
+ return;
59
+ chunks.push({ file: normalizedRelative(projectRoot, file), source: fs.readFileSync(file, 'utf8') });
60
+ });
61
+ return chunks;
61
62
  }
62
63
  function normalizedRelative(root, target) {
63
64
  return path.relative(root, target).split(path.sep).join('/') || '.';
@@ -87,6 +88,15 @@ export function validateFrontendAnalyticsProject(projectDir) {
87
88
  if (path.basename(file) === 'package.json')
88
89
  packageFiles.push(file);
89
90
  });
91
+ const packages = new Map();
92
+ for (const manifestFile of packageFiles) {
93
+ try {
94
+ const manifest = readJson(manifestFile);
95
+ if (typeof manifest.name === 'string')
96
+ packages.set(manifest.name, { root: path.dirname(manifestFile), manifest });
97
+ }
98
+ catch { /* Invalid manifests are ignored as application candidates below. */ }
99
+ }
90
100
  const applications = packageFiles.flatMap((manifestFile) => {
91
101
  let manifest;
92
102
  try {
@@ -100,19 +110,45 @@ export function validateFrontendAnalyticsProject(projectDir) {
100
110
  return [];
101
111
  const applicationRoot = path.dirname(manifestFile);
102
112
  const applicationPath = normalizedRelative(root, applicationRoot);
103
- const source = scanSource(applicationRoot);
113
+ const sourceRoots = [applicationRoot];
114
+ const visited = new Set([applicationRoot]);
115
+ for (let index = 0; index < sourceRoots.length; index += 1) {
116
+ const currentRoot = sourceRoots[index];
117
+ const current = index === 0
118
+ ? manifest
119
+ : [...packages.values()].find((entry) => entry.root === currentRoot)?.manifest;
120
+ if (!current)
121
+ continue;
122
+ const dependencies = { ...(current.dependencies ?? {}), ...(current.devDependencies ?? {}) };
123
+ const directSource = scanSource(root, [currentRoot]).map((item) => item.source).join('\n');
124
+ for (const dependencyName of Object.keys(dependencies)) {
125
+ const dependency = packages.get(dependencyName);
126
+ if (!dependency || visited.has(dependency.root))
127
+ continue;
128
+ const escaped = dependencyName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
129
+ if (!new RegExp(`['\"]${escaped}(?:/[^'\"]*)?['\"]`).test(directSource))
130
+ continue;
131
+ visited.add(dependency.root);
132
+ sourceRoots.push(dependency.root);
133
+ }
134
+ }
135
+ const files = scanSource(root, sourceRoots);
104
136
  const decision = decisions.applications?.[applicationPath];
105
- const signals = {
106
- provider: /DuctapeProvider/.test(source),
107
- authenticatedSessions: /useSession|\.sessions\.|sessionToken|refreshToken/.test(source),
108
- analytics: /useAnalytics|\.analytics\./.test(source),
109
- identify: /(?:analytics\.)?identify\s*\(/.test(source),
110
- clearSession: /(?:analytics\.)?clearSession\s*\(/.test(source),
111
- pageview: /(?:analytics\.)?pageview\s*\(/.test(source),
112
- namedEvents: /(?:analytics\.)?track\s*\(/.test(source),
113
- autoCapture: /enableAutoCapture\s*\(/.test(source),
114
- clientFailure: /client[_\-. ]error|frontend[_\-. ]error|error_boundary|unhandledrejection/i.test(source),
137
+ const patterns = {
138
+ provider: /DuctapeProvider/, authenticatedSessions: /useSession|\.sessions\.|sessionToken|refreshToken/,
139
+ analytics: /useAnalytics|\.analytics\./, identify: /(?:analytics\.)?identify\s*\(/,
140
+ clearSession: /(?:analytics\.)?clearSession\s*\(/, pageview: /(?:analytics\.)?pageview\s*\(/,
141
+ namedEvents: /(?:analytics\.)?track\s*\(/, autoCapture: /enableAutoCapture\s*\(/,
142
+ clientFailure: /client[_\-. ]error|frontend[_\-. ]error|error_boundary|unhandledrejection/i,
143
+ settingsSurface: /settings|preferences|notificationpreferences/i,
144
+ settingsLiveRead: /\b(?:get|load|fetch|request)\s*[:=(]|method\s*:\s*['"]GET['"]/i,
145
+ settingsLiveWrite: /\b(?:update|save|mutate|request)\s*[:=(]|method\s*:\s*['"](?:POST|PUT|PATCH)['"]/i,
146
+ settingsLoading: /\b(?:loading|isLoading|pending|isPending)\b/i,
147
+ settingsFailure: /\b(?:error|failure|failed)\b/i,
148
+ settingsRetry: /\b(?:retry|refetch|reload|tryAgain)\b/i,
115
149
  };
150
+ const evidence = Object.fromEntries(Object.entries(patterns).map(([name, pattern]) => [name, files.filter((item) => pattern.test(item.source)).map((item) => item.file)]));
151
+ const signals = Object.fromEntries(Object.entries(evidence).map(([name, matches]) => [name, matches.length > 0]));
116
152
  const errors = [];
117
153
  const warnings = [];
118
154
  if (decisionError)
@@ -147,8 +183,22 @@ export function validateFrontendAnalyticsProject(projectDir) {
147
183
  if (!signals.clientFailure) {
148
184
  warnings.push('No sanitized client-failure analytics signal was detected; review error boundaries and unhandled failures.');
149
185
  }
186
+ if (signals.settingsSurface) {
187
+ if (!signals.settingsLiveRead)
188
+ errors.push('A settings surface was found without a statically discoverable canonical read path.');
189
+ if (!signals.settingsLiveWrite)
190
+ errors.push('A settings surface was found without a statically discoverable persisted mutation path.');
191
+ if (!signals.settingsLoading)
192
+ errors.push('A settings surface was found without a loading/pending state.');
193
+ if (!signals.settingsFailure)
194
+ errors.push('A settings surface was found without a failure state.');
195
+ if (!signals.settingsRetry)
196
+ errors.push('A settings surface was found without a retry/refetch path.');
197
+ if (!decision.settingsContractsReviewed)
198
+ errors.push('Settings payload keys must be reviewed against the current registered Feature input schema; record settingsContractsReviewed: true after that review.');
199
+ }
150
200
  }
151
- return [{ path: applicationPath, framework, decision, signals, errors, warnings }];
201
+ return [{ path: applicationPath, framework, decision, signals, evidence, errors, warnings }];
152
202
  });
153
203
  const errors = applications.reduce((count, app) => count + app.errors.length, 0);
154
204
  const warnings = applications.reduce((count, app) => count + app.warnings.length, 0);
package/dist/index.js CHANGED
@@ -1723,6 +1723,9 @@ const featuresProjectValidationInputSchema = z.object({
1723
1723
  const frontendAnalyticsProjectValidationInputSchema = z.object({
1724
1724
  project_dir: z.string().describe('Absolute path to a frontend project or monorepo. The validator is read-only and never modifies application code.'),
1725
1725
  });
1726
+ const sessionProjectValidationInputSchema = z.object({
1727
+ project_dir: z.string().describe('Absolute path to the project. Performs a read-only SDK session-propagation audit.'),
1728
+ });
1726
1729
  const readOnlyLocalAnnotations = {
1727
1730
  readOnlyHint: true,
1728
1731
  destructiveHint: false,
@@ -2388,6 +2391,23 @@ PRODUCT ANALYTICS
2388
2391
  failures, privacy masking, hidden-state safety, and trace correlation. Keep auto-capture off until
2389
2392
  consent, selectors, masking, and sensitive states have been reviewed.
2390
2393
 
2394
+ LIVE SETTINGS AND FEATURE CONTRACTS
2395
+ A settings screen is incomplete if it renders static/mock values, mutates only local state, or
2396
+ sends independently invented payload keys. Before declaring any persisted preference/settings
2397
+ requirement complete:
2398
+ 1. Identify the owning registered Feature and inspect its exact current input schema. Do not
2399
+ infer the schema from UI labels, old examples, TypeScript names, or domain expectations.
2400
+ 2. Generate or consume the frontend input type from that Feature contract. Literal payload keys
2401
+ must be a subset of declared properties and every required property must be supplied.
2402
+ 3. Use the session-aware Ductape client path (directly or through a reviewed application API),
2403
+ load canonical state on entry, persist every mutation, and render loading, failure, and retry.
2404
+ 4. Verify a real read-after-write round trip in the target environment. A successful animation,
2405
+ toast, local state change, mock response, or TypeScript compilation is not persistence proof.
2406
+ 5. For notification preferences, distinguish channel consent (email, sms, push) from event-category
2407
+ subscriptions. Never submit category labels where the Feature contract accepts channel keys.
2408
+ Completion review MUST reject static/mock settings, missing read or write paths, absent failure/retry
2409
+ states, and payload keys that were not checked against the registered Feature schema.
2410
+
2391
2411
  AUTHENTICATION AND SECURITY
2392
2412
  - Use publishableKey in browser applications. Never ship workspace private keys or privileged
2393
2413
  access keys in frontend bundles.
@@ -6106,6 +6126,10 @@ const frontendAnalyticsProjectValidationHandler = async (args) => {
6106
6126
  };
6107
6127
  }
6108
6128
  };
6129
+ const sessionProjectValidationHandler = async (args) => {
6130
+ const result = runCli('sessions validate --json', args.project_dir);
6131
+ return { content: [{ type: 'text', text: result.output || '(no output)' }], ...(result.success ? {} : { isError: true }) };
6132
+ };
6109
6133
  const cliInputSchema = z.object({
6110
6134
  command: z.string().describe('The ductape CLI command to run, without the leading "ductape" word. ' +
6111
6135
  'Examples: "products list", "products create --name \\"My Product\\" --tag my-product", ' +
@@ -6966,6 +6990,12 @@ async function main() {
6966
6990
  inputSchema: frontendAnalyticsProjectValidationInputSchema,
6967
6991
  annotations: readOnlyLocalAnnotations,
6968
6992
  }, frontendAnalyticsProjectValidationHandler);
6993
+ server.registerTool('ductape_sessions_validate_project', {
6994
+ title: 'Validate Ductape Session Propagation',
6995
+ description: 'Read-only project-wide audit. Every supported SDK call must explicitly pass a session, inherit it from Feature context, or carry a reasoned delegated/system marker.',
6996
+ inputSchema: sessionProjectValidationInputSchema,
6997
+ annotations: readOnlyLocalAnnotations,
6998
+ }, sessionProjectValidationHandler);
6969
6999
  server.registerTool('ductape_function_setup', {
6970
7000
  title: 'Ductape Portable Function Setup',
6971
7001
  description: 'Read-only: generate (without applying) the mandatory secure local + remote runtime setup for application functions used by Features. ' +
@@ -7217,6 +7247,7 @@ async function main() {
7217
7247
  server.tool('ductape_events_validate_project', eventsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, eventsProjectValidationHandler);
7218
7248
  server.tool('ductape_features_validate_project', featuresProjectValidationInputSchema.shape, readOnlyLocalAnnotations, featuresProjectValidationHandler);
7219
7249
  server.tool('ductape_frontend_analytics_validate_project', frontendAnalyticsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, frontendAnalyticsProjectValidationHandler);
7250
+ server.tool('ductape_sessions_validate_project', sessionProjectValidationInputSchema.shape, readOnlyLocalAnnotations, sessionProjectValidationHandler);
7220
7251
  server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupAnnotations, portableFunctionSetupHandler);
7221
7252
  server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
7222
7253
  server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",