@ductape/mcp 0.3.3 → 0.3.5

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/README.md CHANGED
@@ -95,6 +95,12 @@ The server exposes runtime, schema, documentation, CLI, discovery, migration, an
95
95
  - Agents must implement and verify the route; they must not claim remote availability from local registration alone.
96
96
  - Agents must first extract native Ductape primitives from migrated code and reserve Functions for irreducible residual domain logic while preserving original transaction boundaries.
97
97
 
98
+ 6. **`ductape_frontend_analytics_validate_project`**:
99
+ - Read-only audit for deployable applications using `@ductape/react`, `@ductape/vue`, or the browser client.
100
+ - Requires an enabled, deferred, or prohibited decision per application in `ductape/analytics/frontend.json`.
101
+ - When enabled, checks analytics usage, authenticated identity lifecycle, router pageviews, named events, client-failure signals, and auto-capture privacy review.
102
+ - Reports actionable findings without rewriting application source.
103
+
98
104
  The `ductape_cli` MCP tool also exposes public app discovery:
99
105
  `marketplace search <capability>`, `marketplace categories`, and
100
106
  `marketplace get <app_tag>`. Inspect the app before generating or executing an action payload.
@@ -0,0 +1,29 @@
1
+ export type AnalyticsDecisionStatus = 'enabled' | 'deferred' | 'prohibited';
2
+ interface AnalyticsDecision {
3
+ status?: AnalyticsDecisionStatus;
4
+ rationale?: string;
5
+ autoCapture?: 'disabled' | 'reviewed';
6
+ privacyReviewed?: boolean;
7
+ }
8
+ export interface FrontendAnalyticsValidationResult {
9
+ valid: boolean;
10
+ decisionFile: string;
11
+ summary: {
12
+ applications: number;
13
+ errors: number;
14
+ warnings: number;
15
+ };
16
+ applications: Array<{
17
+ path: string;
18
+ framework: 'react' | 'vue' | 'client';
19
+ decision?: AnalyticsDecision;
20
+ signals: Record<string, boolean>;
21
+ evidence: Record<string, string[]>;
22
+ errors: string[];
23
+ warnings: string[];
24
+ }>;
25
+ remediation: string[];
26
+ }
27
+ export declare function validateFrontendAnalyticsProject(projectDir: string): FrontendAnalyticsValidationResult;
28
+ export {};
29
+ //# sourceMappingURL=frontend-analytics-validator.d.ts.map
@@ -0,0 +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,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,CA2HnC"}
@@ -0,0 +1,198 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const IGNORED_DIRECTORIES = new Set([
4
+ '.git', '.next', '.nuxt', '.output', 'build', 'coverage', 'dist', 'node_modules',
5
+ ]);
6
+ const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.vue']);
7
+ const MAX_SOURCE_BYTES = 1_000_000;
8
+ function walk(root, visit) {
9
+ if (!fs.existsSync(root))
10
+ return;
11
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
12
+ if (entry.isSymbolicLink())
13
+ continue;
14
+ const absolute = path.join(root, entry.name);
15
+ if (entry.isDirectory()) {
16
+ if (!IGNORED_DIRECTORIES.has(entry.name))
17
+ walk(absolute, visit);
18
+ }
19
+ else if (entry.isFile()) {
20
+ visit(absolute);
21
+ }
22
+ }
23
+ }
24
+ function readJson(file) {
25
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
26
+ }
27
+ function packageFramework(manifest) {
28
+ const runtimeDependencies = manifest.dependencies ?? {};
29
+ const dependencies = {
30
+ ...runtimeDependencies,
31
+ ...(manifest.devDependencies ?? {}),
32
+ };
33
+ const scripts = Object.values(manifest.scripts ?? {}).join(' ');
34
+ const isApplication = Boolean(runtimeDependencies['react-dom'] ||
35
+ runtimeDependencies.vue ||
36
+ runtimeDependencies.svelte ||
37
+ runtimeDependencies['@angular/core'] ||
38
+ /(?:^|\s)(?:vite|next|nuxt|react-scripts)(?:\s|$)/.test(scripts) ||
39
+ manifest.browser);
40
+ if (!isApplication)
41
+ return undefined;
42
+ if (dependencies['@ductape/react'])
43
+ return 'react';
44
+ if (dependencies['@ductape/vue'])
45
+ return 'vue';
46
+ if (dependencies['@ductape/client'] && (dependencies.react || dependencies.vue))
47
+ return 'client';
48
+ return undefined;
49
+ }
50
+ function scanSource(projectRoot, roots) {
51
+ const chunks = [];
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;
62
+ }
63
+ function normalizedRelative(root, target) {
64
+ return path.relative(root, target).split(path.sep).join('/') || '.';
65
+ }
66
+ export function validateFrontendAnalyticsProject(projectDir) {
67
+ const root = path.resolve(projectDir);
68
+ if (!path.isAbsolute(projectDir) || !fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
69
+ throw new Error('project_dir must be an existing absolute project directory.');
70
+ }
71
+ const decisionFile = path.join(root, 'ductape', 'analytics', 'frontend.json');
72
+ let decisions = {};
73
+ let decisionError;
74
+ if (fs.existsSync(decisionFile)) {
75
+ try {
76
+ const parsed = readJson(decisionFile);
77
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
78
+ throw new Error('top level must be an object');
79
+ }
80
+ decisions = parsed;
81
+ }
82
+ catch (error) {
83
+ decisionError = `Invalid decision file: ${error instanceof Error ? error.message : String(error)}`;
84
+ }
85
+ }
86
+ const packageFiles = [];
87
+ walk(root, (file) => {
88
+ if (path.basename(file) === 'package.json')
89
+ packageFiles.push(file);
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
+ }
100
+ const applications = packageFiles.flatMap((manifestFile) => {
101
+ let manifest;
102
+ try {
103
+ manifest = readJson(manifestFile);
104
+ }
105
+ catch {
106
+ return [];
107
+ }
108
+ const framework = packageFramework(manifest);
109
+ if (!framework)
110
+ return [];
111
+ const applicationRoot = path.dirname(manifestFile);
112
+ const applicationPath = normalizedRelative(root, 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);
136
+ const decision = decisions.applications?.[applicationPath];
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
+ };
144
+ const evidence = Object.fromEntries(Object.entries(patterns).map(([name, pattern]) => [name, files.filter((item) => pattern.test(item.source)).map((item) => item.file)]));
145
+ const signals = Object.fromEntries(Object.entries(evidence).map(([name, matches]) => [name, matches.length > 0]));
146
+ const errors = [];
147
+ const warnings = [];
148
+ if (decisionError)
149
+ errors.push(decisionError);
150
+ if (!decision) {
151
+ errors.push(`Record this application in ${normalizedRelative(root, decisionFile)}.`);
152
+ }
153
+ else if (!['enabled', 'deferred', 'prohibited'].includes(String(decision.status))) {
154
+ errors.push('Decision status must be enabled, deferred, or prohibited.');
155
+ }
156
+ else if (decision.status !== 'enabled' && !decision.rationale?.trim()) {
157
+ errors.push(`${decision.status} analytics requires a non-empty rationale.`);
158
+ }
159
+ else if (decision.status === 'enabled') {
160
+ if (!decision.privacyReviewed)
161
+ errors.push('Enabled analytics requires privacyReviewed: true.');
162
+ if (!signals.analytics)
163
+ errors.push('No Ductape analytics API usage was found.');
164
+ if (!signals.pageview)
165
+ errors.push('No initial/router pageview lifecycle was found.');
166
+ if (!signals.namedEvents)
167
+ errors.push('No reviewed named product event was found.');
168
+ if (signals.authenticatedSessions && !signals.identify) {
169
+ errors.push('Authenticated sessions exist but identify(fullSessionToken) was not found.');
170
+ }
171
+ if (signals.authenticatedSessions && !signals.clearSession) {
172
+ errors.push('Authenticated sessions exist but clearSession() was not found.');
173
+ }
174
+ if (signals.autoCapture && decision.autoCapture !== 'reviewed') {
175
+ errors.push('Auto-capture is enabled without a recorded reviewed decision.');
176
+ }
177
+ if (!signals.clientFailure) {
178
+ warnings.push('No sanitized client-failure analytics signal was detected; review error boundaries and unhandled failures.');
179
+ }
180
+ }
181
+ return [{ path: applicationPath, framework, decision, signals, evidence, errors, warnings }];
182
+ });
183
+ const errors = applications.reduce((count, app) => count + app.errors.length, 0);
184
+ const warnings = applications.reduce((count, app) => count + app.warnings.length, 0);
185
+ return {
186
+ valid: errors === 0,
187
+ decisionFile,
188
+ summary: { applications: applications.length, errors, warnings },
189
+ applications,
190
+ remediation: [
191
+ 'Read ductape_docs({ topic: "frontend-analytics" }) before changing application code.',
192
+ 'Create ductape/analytics/frontend.json with one applications entry per reported relative path.',
193
+ 'Choose enabled, deferred, or prohibited explicitly; never treat silent omission as completion.',
194
+ 'When enabled, keep auto-capture off until consent, masking, selectors, and sensitive states are reviewed.',
195
+ 'This validator is read-only and must not be used as an automatic codemod.',
196
+ ],
197
+ };
198
+ }
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import { normalizeLiveActionContract } from './action-contract.js';
19
19
  import { runtimeInputRecovery } from './runtime-recovery.js';
20
20
  import { executeViaProxy, generateExecutablePayload, getAssetSchemas, } from './proxy-client.js';
21
21
  import { EVENTS_DELIVERY_SEMANTICS, EVENTS_IMPLEMENTATION_WARNING, eventsCapabilityOverview, inspectEventsComponent, searchEventsCapabilities, } from './events-capabilities.js';
22
+ import { validateFrontendAnalyticsProject } from './frontend-analytics-validator.js';
22
23
  const MODULES = [
23
24
  'product', 'app', 'databases', 'graph', 'webhooks', 'notifications',
24
25
  'events', 'messageBrokers', 'storage', 'vector', 'caches', 'sessions', 'quotas',
@@ -365,6 +366,9 @@ SETUP — register once in AppModule:
365
366
  env: process.env.NODE_ENV === 'production' ? 'prd' : 'snd',
366
367
  redisUrl: process.env.DUCTAPE_REDIS_URL, // required — no dispatch() works without this
367
368
  runtimeSync: { intervalMs: 30_000, jitter: 0.2 },
369
+ // Resolve only the original opaque session retained by the authentication guard.
370
+ sessionResolver: (context) =>
371
+ context.switchToHttp().getRequest().actor?.ductapeSession,
368
372
  }),
369
373
  ],
370
374
  })
@@ -380,9 +384,22 @@ SETUP — register once in AppModule:
380
384
  env: cfg.get('DUCTAPE_ENV'),
381
385
  redisUrl: cfg.get('DUCTAPE_REDIS_URL'), // required — no dispatch() works without this
382
386
  runtimeSync: { intervalMs: 30_000, jitter: 0.2 },
387
+ sessionResolver: (context) =>
388
+ context.switchToHttp().getRequest().actor?.ductapeSession,
383
389
  }),
384
390
  })
385
391
 
392
+ SESSION-AWARE NESTJS EXECUTION:
393
+ Configure sessionResolver once. It runs after guards and stores the returned opaque token in
394
+ request-scoped context. @Feature.Execute, @Feature.Dispatch, @Api, @ApiDispatch, Events,
395
+ notifications, jobs, agents, and session-capable injected resource handles inherit it without
396
+ adding it to business input. Do not return a session from a decorated method or put it in DTOs.
397
+ The resolver must return the exact original "session-tag:jwt", not decoded claims or a bearer
398
+ header. For intentional webhook, scheduled, or broker system work, set
399
+ sessionContext: "system" on session-capable decorators; this suppresses request inheritance.
400
+ Check the installed @ductape/nestjs types before generating this configuration: 0.1.12 does not
401
+ yet contain sessionResolver/sessionContext and must be upgraded to the next published release.
402
+
386
403
  Environment variable (add to .env and deployment secrets):
387
404
  DUCTAPE_REDIS_URL=redis://localhost:6379 # local dev
388
405
  DUCTAPE_REDIS_URL=rediss://:<password>@host:6380 # managed Redis (TLS)
@@ -1317,7 +1334,7 @@ function buildSdkInvocationArgs(payload) {
1317
1334
  function operationAcceptsSession(operationFamily, method) {
1318
1335
  const family = operationFamily.toLowerCase();
1319
1336
  const sessionFamilies = new Set([
1320
- 'action', 'features', 'feature', 'database', 'graph', 'vector', 'storage',
1337
+ 'action', 'api', 'app', 'apps', 'features', 'feature', 'database', 'graph', 'vector', 'storage',
1321
1338
  'notification', 'messaging', 'broker', 'events', 'event', 'quota', 'fallback',
1322
1339
  ]);
1323
1340
  if (!sessionFamilies.has(family))
@@ -1450,7 +1467,7 @@ function buildDatabaseActionFeatureExample(targets, contract) {
1450
1467
  let authState = 'unknown';
1451
1468
  let workspaceSynced = false;
1452
1469
  const ADMIN_SUBCOMMANDS = [
1453
- 'login', 'logout', 'whoami',
1470
+ 'login', 'logout', 'whoami', 'doctor',
1454
1471
  'profiles',
1455
1472
  'workspaces',
1456
1473
  'link', 'unlink', 'init',
@@ -1703,6 +1720,12 @@ const eventsProjectValidationInputSchema = z.object({
1703
1720
  const featuresProjectValidationInputSchema = z.object({
1704
1721
  project_dir: z.string().describe('Absolute path to the linked Ductape project containing ductape/features/. The validator is read-only.'),
1705
1722
  });
1723
+ const frontendAnalyticsProjectValidationInputSchema = z.object({
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
+ });
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
+ });
1706
1729
  const readOnlyLocalAnnotations = {
1707
1730
  readOnlyHint: true,
1708
1731
  destructiveHint: false,
@@ -2328,13 +2351,13 @@ PARITY CLAIMS
2328
2351
  DUCTAPE FRONTEND SDK GUIDE
2329
2352
 
2330
2353
  Choose one integration package:
2331
- React 17+ — npm install @ductape/react
2354
+ React 17+ — npm install @ductape/react@latest
2332
2355
  Provider and hooks built on @ductape/client.
2333
2356
  Continue with ductape_docs({ topic: "react" }).
2334
- Vue 3+ — npm install @ductape/vue
2357
+ Vue 3+ — npm install @ductape/vue@latest
2335
2358
  Plugin and composables built on @ductape/client.
2336
2359
  Continue with ductape_docs({ topic: "vue" }).
2337
- Other UI — npm install @ductape/client
2360
+ Other UI — npm install @ductape/client@latest
2338
2361
  frameworks Use directly with Svelte, Angular, vanilla JavaScript, or a custom adapter.
2339
2362
  Continue with ductape_docs({ topic: "client" }).
2340
2363
 
@@ -2359,8 +2382,14 @@ SHARED APPLICATION LIFECYCLE
2359
2382
 
2360
2383
  PRODUCT ANALYTICS
2361
2384
  Frontend product analytics complements backend session propagation; neither replaces the other.
2362
- Continue with ductape_docs({ topic: "frontend-analytics" }) for identity lifecycle, pageviews,
2363
- custom events, privacy masking, hidden-state safety, trace correlation, and package-version checks.
2385
+ This is a required design decision for every Ductape frontend, not an optional afterthought.
2386
+ Always continue with ductape_docs({ topic: "frontend-analytics" }), then run
2387
+ ductape_frontend_analytics_validate_project({ project_dir: "<absolute-project-root>" }). Record
2388
+ one explicit enabled/deferred/prohibited choice per detected application in
2389
+ ductape/analytics/frontend.json. Silent omission is a failed frontend build/audit outcome.
2390
+ When enabled, wire identity lifecycle, router pageviews, reviewed named events, sanitized client
2391
+ failures, privacy masking, hidden-state safety, and trace correlation. Keep auto-capture off until
2392
+ consent, selectors, masking, and sensitive states have been reviewed.
2364
2393
 
2365
2394
  AUTHENTICATION AND SECURITY
2366
2395
  - Use publishableKey in browser applications. Never ship workspace private keys or privileged
@@ -3545,6 +3574,21 @@ SESSION PROPAGATION
3545
3574
  Feature execution should receive the same explicit actor classification. Never log, serialize
3546
3575
  into business payloads, or attach the raw session/refresh token to traces or error messages.
3547
3576
 
3577
+ FAIL-CLOSED NORMAL SDK RULE
3578
+ Before emitting or approving ordinary @ductape/sdk code for immediate authenticated work, verify
3579
+ that every session-capable runtime call receives the exact ActorContext.session through its
3580
+ top-level session option. This includes api.run/api.dispatch, feature.execute/feature.dispatch,
3581
+ database primitives and saved actions, graph/vector primitives and saved actions, storage,
3582
+ Events, notifications, agents, quotas, and fallbacks where the installed types accept session.
3583
+ Never substitute a session tag, decoded claims, user ID, Authorization header, refresh token, or
3584
+ business-input field. If the original opaque token is unavailable, stop and report the missing
3585
+ propagation boundary; do not silently emit an unattributed user-context call. The only omission
3586
+ allowed is an explicitly classified system context or a version-verified approved delegated flow.
3587
+
3588
+ A Feature receives the session at its execution boundary. Its nested ctx.api, ctx.database,
3589
+ ctx.graph, ctx.vector, ctx.storage, ctx.events, and ctx.notifications calls inherit ctx.session;
3590
+ do not serialize ctx.session into their input objects or hardcode it in the Feature definition.
3591
+
3548
3592
  SECURITY BOUNDARY FOR DURABLE WORK
3549
3593
  The current SDK accepts a session string on dispatch, but it does not expose an immutable
3550
3594
  attribution snapshot API, an expired-token attribution contract, or a general delegated-session
@@ -5067,7 +5111,7 @@ connect to the proxy's /realtime gateway. Never use this package on the server
5067
5111
  for server-side code.
5068
5112
 
5069
5113
  INSTALL
5070
- npm install @ductape/client
5114
+ npm install @ductape/client@latest
5071
5115
 
5072
5116
  INITIALIZATION
5073
5117
  import { createClient } from '@ductape/client';
@@ -5230,7 +5274,7 @@ React hooks and context provider for Ductape. Wraps @ductape/client.
5230
5274
  Requires react >= 17.
5231
5275
 
5232
5276
  INSTALL
5233
- npm install @ductape/react
5277
+ npm install @ductape/react@latest
5234
5278
 
5235
5279
  SETUP — wrap your app root with DuctapeProvider
5236
5280
  import { DuctapeProvider } from '@ductape/react';
@@ -5434,7 +5478,7 @@ Vue 3 composables and plugin for Ductape. Wraps @ductape/client.
5434
5478
  Requires vue >= 3.
5435
5479
 
5436
5480
  INSTALL
5437
- npm install @ductape/vue
5481
+ npm install @ductape/vue@latest
5438
5482
 
5439
5483
  SETUP — install the plugin at app root
5440
5484
  // main.ts
@@ -6047,6 +6091,28 @@ const featuresProjectValidationHandler = async (args) => {
6047
6091
  ...(result.success ? {} : { isError: true }),
6048
6092
  };
6049
6093
  };
6094
+ const frontendAnalyticsProjectValidationHandler = async (args) => {
6095
+ try {
6096
+ const result = validateFrontendAnalyticsProject(args.project_dir);
6097
+ return {
6098
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
6099
+ ...(result.valid ? {} : { isError: true }),
6100
+ };
6101
+ }
6102
+ catch (error) {
6103
+ return {
6104
+ content: [{
6105
+ type: 'text',
6106
+ text: error instanceof Error ? error.message : String(error),
6107
+ }],
6108
+ isError: true,
6109
+ };
6110
+ }
6111
+ };
6112
+ const sessionProjectValidationHandler = async (args) => {
6113
+ const result = runCli('sessions validate --json', args.project_dir);
6114
+ return { content: [{ type: 'text', text: result.output || '(no output)' }], ...(result.success ? {} : { isError: true }) };
6115
+ };
6050
6116
  const cliInputSchema = z.object({
6051
6117
  command: z.string().describe('The ductape CLI command to run, without the leading "ductape" word. ' +
6052
6118
  'Examples: "products list", "products create --name \\"My Product\\" --tag my-product", ' +
@@ -6898,6 +6964,21 @@ async function main() {
6898
6964
  inputSchema: featuresProjectValidationInputSchema,
6899
6965
  annotations: readOnlyLocalAnnotations,
6900
6966
  }, featuresProjectValidationHandler);
6967
+ server.registerTool('ductape_frontend_analytics_validate_project', {
6968
+ title: 'Validate Ductape Frontend Analytics',
6969
+ description: 'Read-only audit of every frontend package using @ductape/react, @ductape/vue, or the browser client. ' +
6970
+ 'Requires an explicit enabled/deferred/prohibited decision per application and validates analytics identity, ' +
6971
+ 'logout, pageview, named-event, client-failure, and auto-capture privacy signals when enabled. ' +
6972
+ 'Returns actionable findings and never modifies source.',
6973
+ inputSchema: frontendAnalyticsProjectValidationInputSchema,
6974
+ annotations: readOnlyLocalAnnotations,
6975
+ }, frontendAnalyticsProjectValidationHandler);
6976
+ server.registerTool('ductape_sessions_validate_project', {
6977
+ title: 'Validate Ductape Session Propagation',
6978
+ 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.',
6979
+ inputSchema: sessionProjectValidationInputSchema,
6980
+ annotations: readOnlyLocalAnnotations,
6981
+ }, sessionProjectValidationHandler);
6901
6982
  server.registerTool('ductape_function_setup', {
6902
6983
  title: 'Ductape Portable Function Setup',
6903
6984
  description: 'Read-only: generate (without applying) the mandatory secure local + remote runtime setup for application functions used by Features. ' +
@@ -7148,6 +7229,8 @@ async function main() {
7148
7229
  server.tool('ductape_events_topic_setup', eventsTopicSetupInputSchema.shape, readOnlyLocalAnnotations, eventsTopicSetupHandler);
7149
7230
  server.tool('ductape_events_validate_project', eventsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, eventsProjectValidationHandler);
7150
7231
  server.tool('ductape_features_validate_project', featuresProjectValidationInputSchema.shape, readOnlyLocalAnnotations, featuresProjectValidationHandler);
7232
+ server.tool('ductape_frontend_analytics_validate_project', frontendAnalyticsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, frontendAnalyticsProjectValidationHandler);
7233
+ server.tool('ductape_sessions_validate_project', sessionProjectValidationInputSchema.shape, readOnlyLocalAnnotations, sessionProjectValidationHandler);
7151
7234
  server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupAnnotations, portableFunctionSetupHandler);
7152
7235
  server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
7153
7236
  server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
package/docs/TOOLS.md CHANGED
@@ -74,6 +74,19 @@ manifest, envelope registry, or custom event registry.
74
74
 
75
75
  ---
76
76
 
77
+ ## Tool: `ductape_frontend_analytics_validate_project`
78
+
79
+ Audits every deployable Ductape React, Vue, or browser-client application under an absolute project
80
+ directory. Each application must have an explicit entry in `ductape/analytics/frontend.json` with
81
+ status `enabled`, `deferred`, or `prohibited`. Deferred and prohibited choices require a rationale.
82
+
83
+ Enabled applications are checked for Ductape analytics usage, router pageviews, named events,
84
+ `identify(fullSessionToken)` after authenticated session creation/refresh, `clearSession()` on
85
+ logout/account switching, sanitized client-failure telemetry, and reviewed auto-capture. The tool
86
+ is read-only and never edits application source.
87
+
88
+ ---
89
+
77
90
  ## Tool: `ductape_function_setup`
78
91
 
79
92
  Produces the mandatory local registry and signed HTTPS exposure plan for portable application
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsc",
18
- "test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-paystack-action-schema.mjs && node scripts/check-portable-functions.mjs && node scripts/check-feature-control-flow.mjs && node scripts/check-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs && node scripts/check-asset-file-guidance.mjs && node scripts/check-database-action-contract-guidance.mjs && node scripts/check-runtime-sync-guidance.mjs && node scripts/check-runtime-input-recovery.mjs && node scripts/check-resilience-health-guidance.mjs",
18
+ "test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-doctor-e2e.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-frontend-analytics-validator.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-paystack-action-schema.mjs && node scripts/check-portable-functions.mjs && node scripts/check-feature-control-flow.mjs && node scripts/check-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs && node scripts/check-asset-file-guidance.mjs && node scripts/check-database-action-contract-guidance.mjs && node scripts/check-runtime-sync-guidance.mjs && node scripts/check-runtime-input-recovery.mjs && node scripts/check-resilience-health-guidance.mjs && node scripts/check-session-propagation-guidance.mjs",
19
19
  "start": "node dist/index.js",
20
20
  "dev": "tsx src/index.ts"
21
21
  },