visus-mcp 0.2.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.claude/settings.local.json +22 -0
  2. package/LINKEDIN-STRATEGY.md +367 -0
  3. package/README.md +491 -16
  4. package/ROADMAP.md +214 -34
  5. package/SECURITY-AUDIT-v1.md +277 -0
  6. package/STATUS.md +801 -42
  7. package/TROUBLESHOOT-AUTH-20260322-2019.md +291 -0
  8. package/TROUBLESHOOT-JEST-20260323-1357.md +139 -0
  9. package/TROUBLESHOOT-LAMBDA-20260322-1945.md +183 -0
  10. package/VISUS-CLAUDE-CODE-PROMPT.md +1 -1
  11. package/VISUS-PROJECT-PLAN.md +7 -0
  12. package/dist/browser/playwright-renderer.d.ts.map +1 -1
  13. package/dist/browser/playwright-renderer.js +7 -0
  14. package/dist/browser/playwright-renderer.js.map +1 -1
  15. package/dist/browser/reader.d.ts +31 -0
  16. package/dist/browser/reader.d.ts.map +1 -0
  17. package/dist/browser/reader.js +98 -0
  18. package/dist/browser/reader.js.map +1 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +37 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/lambda-handler.d.ts +0 -6
  24. package/dist/lambda-handler.d.ts.map +1 -1
  25. package/dist/lambda-handler.js +97 -25
  26. package/dist/lambda-handler.js.map +1 -1
  27. package/dist/sanitizer/framework-mapper.d.ts +22 -0
  28. package/dist/sanitizer/framework-mapper.d.ts.map +1 -0
  29. package/dist/sanitizer/framework-mapper.js +296 -0
  30. package/dist/sanitizer/framework-mapper.js.map +1 -0
  31. package/dist/sanitizer/index.d.ts +10 -2
  32. package/dist/sanitizer/index.d.ts.map +1 -1
  33. package/dist/sanitizer/index.js +22 -6
  34. package/dist/sanitizer/index.js.map +1 -1
  35. package/dist/sanitizer/patterns.js +1 -1
  36. package/dist/sanitizer/patterns.js.map +1 -1
  37. package/dist/sanitizer/pii-allowlist.d.ts +49 -0
  38. package/dist/sanitizer/pii-allowlist.d.ts.map +1 -0
  39. package/dist/sanitizer/pii-allowlist.js +231 -0
  40. package/dist/sanitizer/pii-allowlist.js.map +1 -0
  41. package/dist/sanitizer/pii-redactor.d.ts +13 -1
  42. package/dist/sanitizer/pii-redactor.d.ts.map +1 -1
  43. package/dist/sanitizer/pii-redactor.js +26 -2
  44. package/dist/sanitizer/pii-redactor.js.map +1 -1
  45. package/dist/sanitizer/severity-classifier.d.ts +33 -0
  46. package/dist/sanitizer/severity-classifier.d.ts.map +1 -0
  47. package/dist/sanitizer/severity-classifier.js +113 -0
  48. package/dist/sanitizer/severity-classifier.js.map +1 -0
  49. package/dist/sanitizer/threat-reporter.d.ts +65 -0
  50. package/dist/sanitizer/threat-reporter.d.ts.map +1 -0
  51. package/dist/sanitizer/threat-reporter.js +160 -0
  52. package/dist/sanitizer/threat-reporter.js.map +1 -0
  53. package/dist/tools/fetch-structured.d.ts +5 -0
  54. package/dist/tools/fetch-structured.d.ts.map +1 -1
  55. package/dist/tools/fetch-structured.js +59 -8
  56. package/dist/tools/fetch-structured.js.map +1 -1
  57. package/dist/tools/fetch.d.ts +5 -0
  58. package/dist/tools/fetch.d.ts.map +1 -1
  59. package/dist/tools/fetch.js +43 -9
  60. package/dist/tools/fetch.js.map +1 -1
  61. package/dist/tools/read.d.ts +51 -0
  62. package/dist/tools/read.d.ts.map +1 -0
  63. package/dist/tools/read.js +127 -0
  64. package/dist/tools/read.js.map +1 -0
  65. package/dist/tools/search.d.ts +45 -0
  66. package/dist/tools/search.d.ts.map +1 -0
  67. package/dist/tools/search.js +220 -0
  68. package/dist/tools/search.js.map +1 -0
  69. package/dist/types.d.ts +74 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js.map +1 -1
  72. package/dist/utils/format-converter.d.ts +39 -0
  73. package/dist/utils/format-converter.d.ts.map +1 -0
  74. package/dist/utils/format-converter.js +191 -0
  75. package/dist/utils/format-converter.js.map +1 -0
  76. package/dist/utils/truncate.d.ts +26 -0
  77. package/dist/utils/truncate.d.ts.map +1 -0
  78. package/dist/utils/truncate.js +54 -0
  79. package/dist/utils/truncate.js.map +1 -0
  80. package/infrastructure/stack.ts +55 -6
  81. package/jest.config.js +3 -0
  82. package/package.json +9 -2
  83. package/src/browser/playwright-renderer.ts +8 -0
  84. package/src/browser/reader.ts +129 -0
  85. package/src/index.ts +49 -5
  86. package/src/lambda-handler.ts +131 -26
  87. package/src/sanitizer/framework-mapper.ts +347 -0
  88. package/src/sanitizer/index.ts +28 -6
  89. package/src/sanitizer/patterns.ts +1 -1
  90. package/src/sanitizer/pii-allowlist.ts +273 -0
  91. package/src/sanitizer/pii-redactor.ts +43 -2
  92. package/src/sanitizer/severity-classifier.ts +132 -0
  93. package/src/sanitizer/threat-reporter.ts +261 -0
  94. package/src/tools/fetch-structured.ts +63 -8
  95. package/src/tools/fetch.ts +45 -9
  96. package/src/tools/read.ts +143 -0
  97. package/src/tools/search.ts +263 -0
  98. package/src/types.ts +71 -0
  99. package/src/utils/format-converter.ts +236 -0
  100. package/src/utils/truncate.ts +64 -0
  101. package/tests/auth-smoke.test.ts +480 -0
  102. package/tests/fetch-tool.test.ts +595 -2
  103. package/tests/pii-allowlist.test.ts +282 -0
  104. package/tests/reader.test.ts +353 -0
  105. package/tests/sanitizer.test.ts +52 -0
  106. package/tests/search.test.ts +456 -0
  107. package/tests/threat-reporter.test.ts +266 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Format Converter - Content-Type based format detection and conversion
3
+ *
4
+ * Handles format-appropriate conversion based on detected Content-Type.
5
+ * Supports HTML, JSON, XML, and RSS/Atom feeds.
6
+ */
7
+ import { XMLParser } from 'fast-xml-parser';
8
+ /**
9
+ * Detect format from Content-Type header
10
+ *
11
+ * @param contentType - Content-Type header value (e.g., "application/json", "text/html; charset=utf-8")
12
+ * @returns Detected format type
13
+ */
14
+ export function detectFormat(contentType) {
15
+ // Normalize: lowercase and extract MIME type (before semicolon)
16
+ const mimeType = contentType.toLowerCase().split(';')[0].trim();
17
+ // HTML formats
18
+ if (mimeType === 'text/html' || mimeType === 'application/xhtml+xml') {
19
+ return 'html';
20
+ }
21
+ // JSON formats
22
+ if (mimeType === 'application/json' || mimeType === 'text/json') {
23
+ return 'json';
24
+ }
25
+ // RSS/Atom feed formats
26
+ if (mimeType === 'application/rss+xml' ||
27
+ mimeType === 'application/atom+xml' ||
28
+ mimeType === 'application/feed+json') {
29
+ return 'rss';
30
+ }
31
+ // XML formats (must come after RSS check)
32
+ if (mimeType === 'application/xml' || mimeType === 'text/xml') {
33
+ return 'xml';
34
+ }
35
+ // Default to HTML for unknown types
36
+ return 'html';
37
+ }
38
+ /**
39
+ * Convert JSON content to formatted string
40
+ *
41
+ * @param raw - Raw JSON string
42
+ * @returns Formatted JSON string with prefix, or raw string if parse fails
43
+ */
44
+ export function convertJson(raw) {
45
+ try {
46
+ // Parse and re-stringify with 2-space indent for readability
47
+ const parsed = JSON.parse(raw);
48
+ const formatted = JSON.stringify(parsed, null, 2);
49
+ return `JSON Response:\n\n${formatted}`;
50
+ }
51
+ catch (error) {
52
+ // Parse failed, return raw string unchanged
53
+ return raw;
54
+ }
55
+ }
56
+ /**
57
+ * Convert XML content to clean text representation
58
+ *
59
+ * @param raw - Raw XML string
60
+ * @returns Formatted XML representation with prefix, or tag-stripped fallback if parse fails
61
+ */
62
+ export function convertXml(raw) {
63
+ try {
64
+ const parser = new XMLParser({
65
+ ignoreAttributes: false,
66
+ attributeNamePrefix: '@_',
67
+ textNodeName: '#text',
68
+ ignoreDeclaration: true,
69
+ ignorePiTags: true,
70
+ removeNSPrefix: true,
71
+ });
72
+ const parsed = parser.parse(raw);
73
+ const formatted = JSON.stringify(parsed, null, 2);
74
+ return `XML Response:\n\n${formatted}`;
75
+ }
76
+ catch (error) {
77
+ // Parse failed, strip XML tags using regex and return
78
+ const stripped = raw.replace(/<[^>]+>/g, '').trim();
79
+ return `XML Response:\n\n${stripped}`;
80
+ }
81
+ }
82
+ /**
83
+ * Convert RSS/Atom feed content to clean Markdown
84
+ *
85
+ * @param raw - Raw RSS/Atom XML string
86
+ * @returns Formatted Markdown representation, or falls back to convertXml if parse fails
87
+ */
88
+ export function convertRss(raw) {
89
+ try {
90
+ const parser = new XMLParser({
91
+ ignoreAttributes: false,
92
+ attributeNamePrefix: '@_',
93
+ textNodeName: '#text',
94
+ removeNSPrefix: true,
95
+ });
96
+ const parsed = parser.parse(raw);
97
+ // Handle RSS 2.0 format
98
+ if (parsed.rss && parsed.rss.channel) {
99
+ return formatRss2(parsed.rss.channel);
100
+ }
101
+ // Handle Atom format
102
+ if (parsed.feed) {
103
+ return formatAtom(parsed.feed);
104
+ }
105
+ // Handle RSS 1.0 (RDF) format
106
+ if (parsed.rdf && parsed.rdf.channel) {
107
+ return formatRss2(parsed.rdf.channel);
108
+ }
109
+ // Unknown feed format, fall back to XML
110
+ return convertXml(raw);
111
+ }
112
+ catch (error) {
113
+ // Parse failed, fall back to XML converter
114
+ return convertXml(raw);
115
+ }
116
+ }
117
+ /**
118
+ * Format RSS 2.0 feed data as Markdown
119
+ */
120
+ function formatRss2(channel) {
121
+ const title = channel.title || 'Untitled Feed';
122
+ const description = channel.description || '';
123
+ const items = Array.isArray(channel.item) ? channel.item : (channel.item ? [channel.item] : []);
124
+ let markdown = `RSS Feed:\n\n# ${title}\n`;
125
+ if (description) {
126
+ markdown += `${description}\n`;
127
+ }
128
+ markdown += '\n## Items\n\n';
129
+ // Extract up to 10 items
130
+ const itemsToShow = items.slice(0, 10);
131
+ for (const item of itemsToShow) {
132
+ const itemTitle = item.title || 'Untitled';
133
+ const itemLink = item.link || '';
134
+ const itemDescription = item.description || '';
135
+ const itemPubDate = item.pubDate || '';
136
+ // Truncate description to 200 chars
137
+ const truncatedDesc = itemDescription.length > 200
138
+ ? itemDescription.substring(0, 200) + '...'
139
+ : itemDescription;
140
+ markdown += `### ${itemTitle}\n\n`;
141
+ if (truncatedDesc) {
142
+ markdown += `${truncatedDesc}\n\n`;
143
+ }
144
+ if (itemLink) {
145
+ markdown += `Link: ${itemLink}\n`;
146
+ }
147
+ if (itemPubDate) {
148
+ markdown += `Published: ${itemPubDate}\n`;
149
+ }
150
+ markdown += '\n---\n\n';
151
+ }
152
+ return markdown;
153
+ }
154
+ /**
155
+ * Format Atom feed data as Markdown
156
+ */
157
+ function formatAtom(feed) {
158
+ const title = feed.title || 'Untitled Feed';
159
+ const subtitle = feed.subtitle || '';
160
+ const entries = Array.isArray(feed.entry) ? feed.entry : (feed.entry ? [feed.entry] : []);
161
+ let markdown = `RSS Feed:\n\n# ${title}\n`;
162
+ if (subtitle) {
163
+ markdown += `${subtitle}\n`;
164
+ }
165
+ markdown += '\n## Items\n\n';
166
+ // Extract up to 10 entries
167
+ const entriesToShow = entries.slice(0, 10);
168
+ for (const entry of entriesToShow) {
169
+ const entryTitle = entry.title || 'Untitled';
170
+ const entryLink = entry.link ? (entry.link['@_href'] || entry.link) : '';
171
+ const entrySummary = entry.summary || entry.content || '';
172
+ const entryPublished = entry.published || entry.updated || '';
173
+ // Truncate summary to 200 chars
174
+ const truncatedSummary = entrySummary.length > 200
175
+ ? entrySummary.substring(0, 200) + '...'
176
+ : entrySummary;
177
+ markdown += `### ${entryTitle}\n\n`;
178
+ if (truncatedSummary) {
179
+ markdown += `${truncatedSummary}\n\n`;
180
+ }
181
+ if (entryLink) {
182
+ markdown += `Link: ${entryLink}\n`;
183
+ }
184
+ if (entryPublished) {
185
+ markdown += `Published: ${entryPublished}\n`;
186
+ }
187
+ markdown += '\n---\n\n';
188
+ }
189
+ return markdown;
190
+ }
191
+ //# sourceMappingURL=format-converter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-converter.js","sourceRoot":"","sources":["../../src/utils/format-converter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Token-aware content truncation utility
3
+ *
4
+ * Anthropic MCP Directory enforces a 25,000 token response limit.
5
+ * This utility provides safe truncation with token estimation.
6
+ */
7
+ /**
8
+ * Truncate content if it exceeds the token ceiling
9
+ *
10
+ * @param content Content to potentially truncate
11
+ * @returns Truncated content and metadata
12
+ */
13
+ export declare function truncateContent(content: string): {
14
+ content: string;
15
+ truncated: boolean;
16
+ truncated_at_chars?: number;
17
+ };
18
+ /**
19
+ * Estimate token count for a given string
20
+ * Uses conservative 4 chars per token approximation
21
+ *
22
+ * @param text Text to estimate
23
+ * @returns Estimated token count
24
+ */
25
+ export declare function estimateTokens(text: string): number;
26
+ //# sourceMappingURL=truncate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAmBH;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAkBA;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Token-aware content truncation utility
3
+ *
4
+ * Anthropic MCP Directory enforces a 25,000 token response limit.
5
+ * This utility provides safe truncation with token estimation.
6
+ */
7
+ /**
8
+ * Maximum tokens allowed in MCP response (Anthropic Directory limit)
9
+ * We target 24,000 to leave headroom for metadata/JSON structure
10
+ */
11
+ const MAX_TOKENS = 24000;
12
+ /**
13
+ * Conservative token estimation: 1 token ≈ 4 characters
14
+ * This is a safe approximation that errs on the side of caution
15
+ */
16
+ const CHARS_PER_TOKEN = 4;
17
+ /**
18
+ * Maximum characters based on token limit
19
+ */
20
+ const MAX_CHARS = MAX_TOKENS * CHARS_PER_TOKEN; // 96,000 characters
21
+ /**
22
+ * Truncate content if it exceeds the token ceiling
23
+ *
24
+ * @param content Content to potentially truncate
25
+ * @returns Truncated content and metadata
26
+ */
27
+ export function truncateContent(content) {
28
+ if (content.length <= MAX_CHARS) {
29
+ // Content is within limits
30
+ return {
31
+ content,
32
+ truncated: false
33
+ };
34
+ }
35
+ // Content exceeds limit - truncate with warning message
36
+ const truncatedContent = content.substring(0, MAX_CHARS);
37
+ const warningMessage = `\n\n--- CONTENT TRUNCATED ---\nOriginal length: ${content.length} characters (~${Math.ceil(content.length / CHARS_PER_TOKEN)} tokens)\nTruncated to: ${MAX_CHARS} characters (~${MAX_TOKENS} tokens)\nReason: Anthropic MCP Directory enforces a 25,000 token response limit\n`;
38
+ return {
39
+ content: truncatedContent + warningMessage,
40
+ truncated: true,
41
+ truncated_at_chars: MAX_CHARS
42
+ };
43
+ }
44
+ /**
45
+ * Estimate token count for a given string
46
+ * Uses conservative 4 chars per token approximation
47
+ *
48
+ * @param text Text to estimate
49
+ * @returns Estimated token count
50
+ */
51
+ export function estimateTokens(text) {
52
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
53
+ }
54
+ //# sourceMappingURL=truncate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,UAAU,GAAG,KAAK,CAAC;AAEzB;;;GAGG;AACH,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B;;GAEG;AACH,MAAM,SAAS,GAAG,UAAU,GAAG,eAAe,CAAC,CAAC,oBAAoB;AAEpE;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAK7C,IAAI,OAAO,CAAC,MAAM,IAAI,SAAS,EAAE,CAAC;QAChC,2BAA2B;QAC3B,OAAO;YACL,OAAO;YACP,SAAS,EAAE,KAAK;SACjB,CAAC;IACJ,CAAC;IAED,wDAAwD;IACxD,MAAM,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACzD,MAAM,cAAc,GAAG,mDAAmD,OAAO,CAAC,MAAM,iBAAiB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC,2BAA2B,SAAS,iBAAiB,UAAU,oFAAoF,CAAC;IAExS,OAAO;QACL,OAAO,EAAE,gBAAgB,GAAG,cAAc;QAC1C,SAAS,EAAE,IAAI;QACf,kBAAkB,EAAE,SAAS;KAC9B,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC;AAClD,CAAC"}
@@ -19,6 +19,7 @@
19
19
 
20
20
  import * as cdk from 'aws-cdk-lib';
21
21
  import * as lambda from 'aws-cdk-lib/aws-lambda';
22
+ import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs';
22
23
  import * as apigateway from 'aws-cdk-lib/aws-apigateway';
23
24
  import * as cognito from 'aws-cdk-lib/aws-cognito';
24
25
  import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
@@ -69,6 +70,7 @@ export class VisusStack extends cdk.Stack {
69
70
  removalPolicy: environment === 'prod'
70
71
  ? cdk.RemovalPolicy.RETAIN
71
72
  : cdk.RemovalPolicy.DESTROY,
73
+ timeToLiveAttribute: 'ttl', // Auto-delete audit logs after 30 days
72
74
  });
73
75
 
74
76
  // Global Secondary Index for querying by request_id
@@ -142,12 +144,12 @@ export class VisusStack extends cdk.Stack {
142
144
  // Grant KMS decrypt access (for reading encrypted DynamoDB data if needed)
143
145
  kmsKey.grantEncryptDecrypt(lambdaRole);
144
146
 
145
- // Lambda function
146
- const visusFn = new lambda.Function(this, 'VisusFunction', {
147
+ // Lambda function (NodejsFunction with automatic bundling)
148
+ const visusFn = new lambdaNodejs.NodejsFunction(this, 'VisusFunction', {
147
149
  functionName: `visus-mcp-${environment}`,
148
150
  runtime: lambda.Runtime.NODEJS_20_X,
149
- handler: 'index.handler',
150
- code: lambda.Code.fromAsset('dist'), // Build output directory (relative to project root)
151
+ entry: 'src/lambda-handler.ts', // Entry point for bundler
152
+ handler: 'handler', // Export name in the entry file
151
153
  timeout: cdk.Duration.seconds(30), // Playwright page loads can take time
152
154
  memorySize: 1024, // Chromium requires significant memory
153
155
  reservedConcurrentExecutions: environment === 'prod' ? 100 : 10, // RULE 7: Cost protection
@@ -155,12 +157,21 @@ export class VisusStack extends cdk.Stack {
155
157
  environment: {
156
158
  AUDIT_TABLE_NAME: auditTable.tableName,
157
159
  ENVIRONMENT: environment,
160
+ ALLOWED_ORIGINS: 'https://claude.ai,https://app.claude.ai,http://localhost:3000',
158
161
  NODE_OPTIONS: '--enable-source-maps', // For debugging
159
162
  },
160
163
  logRetention: environment === 'prod'
161
164
  ? logs.RetentionDays.ONE_MONTH
162
165
  : logs.RetentionDays.ONE_WEEK,
163
166
  description: `Visus MCP sanitization service (${environment})`,
167
+ bundling: {
168
+ minify: false, // Keep readable for debugging
169
+ sourceMap: true,
170
+ externalModules: [
171
+ 'playwright-core', // Playwright is huge, will be added via layer
172
+ '@sparticuz/chromium', // Chromium binary
173
+ ],
174
+ },
164
175
  });
165
176
 
166
177
  // ========================================
@@ -178,12 +189,44 @@ export class VisusStack extends cdk.Stack {
178
189
  metricsEnabled: true,
179
190
  },
180
191
  defaultCorsPreflightOptions: {
181
- allowOrigins: apigateway.Cors.ALL_ORIGINS, // Phase 2: Open. Phase 3: Restrict to Lateos
182
- allowMethods: apigateway.Cors.ALL_METHODS,
192
+ allowOrigins: [
193
+ 'https://claude.ai',
194
+ 'https://app.claude.ai',
195
+ 'http://localhost:3000', // local dev only
196
+ ],
197
+ allowMethods: ['POST', 'OPTIONS'],
183
198
  allowHeaders: ['Content-Type', 'Authorization'],
184
199
  },
185
200
  });
186
201
 
202
+ // Usage plan with rate limiting and quota
203
+ const usagePlan = api.addUsagePlan('VisusUsagePlan', {
204
+ name: `visus-usage-plan-${environment}`,
205
+ description: 'Rate limiting and quota management for Visus API',
206
+ throttle: {
207
+ rateLimit: 10, // 10 requests per second
208
+ burstLimit: 20, // 20 request burst
209
+ },
210
+ quota: {
211
+ limit: 1000, // 1000 requests per day
212
+ period: apigateway.Period.DAY,
213
+ },
214
+ });
215
+
216
+ // Add deployment stage to usage plan
217
+ usagePlan.addApiStage({
218
+ stage: api.deploymentStage,
219
+ });
220
+
221
+ // Create API key for the usage plan
222
+ const apiKey = api.addApiKey('VisusApiKey', {
223
+ apiKeyName: `visus-api-key-${environment}`,
224
+ description: `API key for Visus ${environment} environment`,
225
+ });
226
+
227
+ // Associate API key with usage plan
228
+ usagePlan.addApiKey(apiKey);
229
+
187
230
  // Cognito authorizer
188
231
  const authorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'VisusAuthorizer', {
189
232
  cognitoUserPools: [userPool],
@@ -244,5 +287,11 @@ export class VisusStack extends cdk.Stack {
244
287
  description: 'Lambda function ARN',
245
288
  exportName: `visus-lambda-arn-${environment}`,
246
289
  });
290
+
291
+ new cdk.CfnOutput(this, 'ApiKeyId', {
292
+ value: apiKey.keyId,
293
+ description: 'API Gateway API Key ID (use aws apigateway get-api-key to retrieve value)',
294
+ exportName: `visus-api-key-id-${environment}`,
295
+ });
247
296
  }
248
297
  }
package/jest.config.js CHANGED
@@ -15,6 +15,9 @@ export default {
15
15
  },
16
16
  testMatch: ['**/tests/**/*.test.ts'],
17
17
  testPathIgnorePatterns: ['/node_modules/', '/dist/'],
18
+ transformIgnorePatterns: [
19
+ 'node_modules/(?!(@mozilla/readability|jsdom|@exodus/bytes|html-encoding-sniffer|@toon-format)/)',
20
+ ],
18
21
  testTimeout: 15000,
19
22
  forceExit: true,
20
23
  detectOpenHandles: false,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "visus-mcp",
3
- "version": "0.2.0",
4
- "description": "Secure web access for Claude sanitizes all web content before it reaches your LLM",
3
+ "version": "0.6.0",
4
+ "description": "Secure, sanitized web access for Claude. Detects prompt injection (43 patterns), redacts PII, renders JavaScript pages, generates NIST AI 600-1 / OWASP / MITRE ATLAS threat reports, and auto-detects JSON/XML/RSS content types.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "visus-mcp": "dist/index.js"
@@ -39,15 +39,22 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "dependencies": {
42
+ "@aws-sdk/client-dynamodb": "^3.1014.0",
43
+ "@aws-sdk/lib-dynamodb": "^3.1014.0",
42
44
  "@modelcontextprotocol/sdk": "^1.0.4",
45
+ "@mozilla/readability": "^0.6.0",
43
46
  "@playwright/test": "^1.58.2",
47
+ "@toon-format/toon": "^2.1.0",
44
48
  "cheerio": "^1.2.0",
49
+ "fast-xml-parser": "^5.5.8",
50
+ "jsdom": "^29.0.1",
45
51
  "playwright": "^1.58.2",
46
52
  "undici": "^7.24.5"
47
53
  },
48
54
  "devDependencies": {
49
55
  "@types/aws-lambda": "^8.10.161",
50
56
  "@types/jest": "^29.5.14",
57
+ "@types/jsdom": "^28.0.1",
51
58
  "@types/node": "^20.19.37",
52
59
  "aws-cdk": "^2.1112.0",
53
60
  "aws-cdk-lib": "^2.244.0",
@@ -126,6 +126,7 @@ async function renderWithLambda(
126
126
  html: body.html,
127
127
  title: body.title,
128
128
  url,
129
+ contentType: 'text/html', // Lambda renderer defaults to HTML
129
130
  text: undefined, // Lambda renderer doesn't extract text
130
131
  });
131
132
 
@@ -164,6 +165,12 @@ async function renderWithFetch(
164
165
 
165
166
  const html = await response.body.text();
166
167
 
168
+ // Capture Content-Type header
169
+ const contentTypeHeader = response.headers['content-type'];
170
+ const contentType = typeof contentTypeHeader === 'string'
171
+ ? contentTypeHeader.split(';')[0].trim() // Remove charset and other params
172
+ : 'text/html'; // Default to HTML if missing
173
+
167
174
  // Extract title using regex (simple fallback)
168
175
  const titleMatch = html.match(/<title[^>]*>(.*?)<\/title>/i);
169
176
  const title = titleMatch ? titleMatch[1].trim() : '';
@@ -172,6 +179,7 @@ async function renderWithFetch(
172
179
  html,
173
180
  title,
174
181
  url,
182
+ contentType,
175
183
  text: undefined,
176
184
  });
177
185
 
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Reader Mode - Mozilla Readability Integration
3
+ *
4
+ * Extracts clean article content from web pages using Mozilla's Readability.js.
5
+ * This module strips navigation, ads, and boilerplate to return main article content.
6
+ *
7
+ * CRITICAL: Content extraction happens BEFORE sanitization. The pipeline is:
8
+ * Playwright renders → Readability extracts → Sanitizer runs → Token ceiling applied
9
+ */
10
+
11
+ import { Readability } from '@mozilla/readability';
12
+ import { JSDOM } from 'jsdom';
13
+ import type { Result } from '../types.js';
14
+ import { Ok, Err } from '../types.js';
15
+
16
+ /**
17
+ * Result from reader mode extraction
18
+ */
19
+ export interface ReaderResult {
20
+ title: string;
21
+ byline: string | null; // Author
22
+ publishedTime: string | null; // ISO timestamp or null
23
+ content: string; // Extracted text content
24
+ excerpt: string | null; // Short summary
25
+ wordCount: number; // Estimated word count
26
+ readerModeAvailable: boolean; // True if Readability succeeded
27
+ }
28
+
29
+ /**
30
+ * Extract clean article content using Mozilla Readability
31
+ *
32
+ * @param html - Rendered HTML from Playwright
33
+ * @param url - Original URL (required for relative link resolution)
34
+ * @returns Result containing extracted article or fallback to full HTML
35
+ */
36
+ export function extractArticle(
37
+ html: string,
38
+ url: string
39
+ ): Result<ReaderResult, Error> {
40
+ try {
41
+ // Parse HTML with jsdom
42
+ const dom = new JSDOM(html, { url });
43
+ const document = dom.window.document;
44
+
45
+ // Attempt extraction with Readability
46
+ const reader = new Readability(document);
47
+ const article = reader.parse();
48
+
49
+ // If Readability succeeds, return extracted content
50
+ if (article && article.textContent) {
51
+ const wordCount = estimateWordCount(article.textContent);
52
+
53
+ return Ok({
54
+ title: article.title || 'Untitled',
55
+ byline: article.byline || null,
56
+ publishedTime: article.publishedTime || null,
57
+ content: article.textContent,
58
+ excerpt: article.excerpt || null,
59
+ wordCount,
60
+ readerModeAvailable: true
61
+ });
62
+ }
63
+
64
+ // Readability failed - fallback to raw text extraction
65
+ const fallbackText = extractFallbackText(document);
66
+ const wordCount = estimateWordCount(fallbackText);
67
+
68
+ // Extract title from <title> tag as fallback
69
+ const titleElement = document.querySelector('title');
70
+ const fallbackTitle = titleElement?.textContent?.trim() || 'Untitled';
71
+
72
+ return Ok({
73
+ title: fallbackTitle,
74
+ byline: null,
75
+ publishedTime: null,
76
+ content: fallbackText,
77
+ excerpt: null,
78
+ wordCount,
79
+ readerModeAvailable: false
80
+ });
81
+
82
+ } catch (error) {
83
+ return Err(
84
+ error instanceof Error
85
+ ? error
86
+ : new Error(`Reader extraction failed: ${String(error)}`)
87
+ );
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Estimate word count from text content
93
+ *
94
+ * @param text - Text content to count
95
+ * @returns Estimated word count
96
+ */
97
+ function estimateWordCount(text: string): number {
98
+ if (!text || text.trim().length === 0) {
99
+ return 0;
100
+ }
101
+
102
+ // Split on whitespace and filter out empty strings
103
+ const words = text.trim().split(/\s+/).filter(word => word.length > 0);
104
+ return words.length;
105
+ }
106
+
107
+ /**
108
+ * Fallback text extraction when Readability fails
109
+ *
110
+ * Extracts visible text from the page, skipping script/style elements.
111
+ *
112
+ * @param document - JSDOM document
113
+ * @returns Extracted text content
114
+ */
115
+ function extractFallbackText(document: Document): string {
116
+ // Remove script and style elements
117
+ const scripts = document.querySelectorAll('script, style, noscript');
118
+ scripts.forEach(el => el.remove());
119
+
120
+ // Extract body text
121
+ const bodyText = document.body?.textContent || '';
122
+
123
+ // Clean up whitespace
124
+ return bodyText
125
+ .split('\n')
126
+ .map(line => line.trim())
127
+ .filter(line => line.length > 0)
128
+ .join('\n');
129
+ }