@bryan-thompson/inspector-assessment 1.4.0 → 1.5.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.
package/README.md CHANGED
@@ -82,8 +82,8 @@ We've built a comprehensive assessment framework on top of the original inspecto
82
82
 
83
83
  Our enhanced fork maintains high code quality standards with comprehensive testing and validation:
84
84
 
85
- - **Test Coverage**: ✅ 582/582 tests passing (100% pass rate)
86
- - **Assessment Module Tests**: 208 tests specifically validating our assessment enhancements
85
+ - **Test Coverage**: ✅ 665/665 tests passing (100% pass rate)
86
+ - **Assessment Module Tests**: 291 tests specifically validating our assessment enhancements (including 83 new MCP Directory compliance tests)
87
87
  - Business logic error detection with confidence scoring
88
88
  - Progressive complexity testing (2 levels: minimal → simple)
89
89
  - Context-aware security testing with zero false positives
@@ -106,11 +106,13 @@ Our enhanced fork maintains high code quality standards with comprehensive testi
106
106
  **Testing Commands**:
107
107
 
108
108
  ```bash
109
- npm test # Run all 582 tests
110
- npm test -- assessment # Run all 208 assessment module tests
109
+ npm test # Run all 665 tests
110
+ npm test -- assessment # Run all 291 assessment module tests
111
111
  npm test -- assessmentService # Run assessment service integration tests (54 tests)
112
112
  npm test -- SecurityAssessor # Run security assessment tests (16 tests)
113
113
  npm test -- FunctionalityAssessor # Run functionality tests (11 tests)
114
+ npm test -- AUPCompliance # Run AUP compliance tests (26 tests)
115
+ npm test -- ToolAnnotation # Run tool annotation tests (13 tests)
114
116
  npm run coverage # Generate coverage report
115
117
  npm run lint # Check code quality
116
118
  ```
@@ -215,7 +217,9 @@ Response: "The answer is 4"
215
217
 
216
218
  **Based on Real-World Testing**: Our methodology has been validated through systematic testing using the taskmanager MCP server as a case study (11 tools tested with 8 backend security patterns, detailed in [ASSESSMENT_METHODOLOGY.md](docs/ASSESSMENT_METHODOLOGY.md)).
217
219
 
218
- **Six Core Assessors** aligned with Anthropic's MCP directory submission requirements:
220
+ **Eleven Core Assessors** aligned with Anthropic's MCP directory submission requirements:
221
+
222
+ **Original MCP Inspector Assessors:**
219
223
 
220
224
  1. **FunctionalityAssessor** (225 lines)
221
225
  - Multi-scenario validation with progressive complexity
@@ -249,7 +253,41 @@ Response: "The answer is 4"
249
253
  - Protocol message format verification
250
254
  - MCP specification adherence
251
255
 
252
- **Recent Refactoring** (2025-10-05): Removed 2,707 lines of out-of-scope assessment modules (HumanInLoopAssessor, PrivacyComplianceAssessor) to focus on core MCP validation requirements. Achieved 100% test pass rate.
256
+ **NEW: MCP Directory Compliance Assessors** (added 2025-12):
257
+
258
+ 7. **AUPComplianceAssessor** - Policy compliance
259
+ - 14 AUP category violation detection (A-N)
260
+ - High-risk domain identification (Healthcare, Financial, Legal, Children)
261
+ - Tool name/description pattern analysis
262
+ - Source code scanning (enhanced mode)
263
+
264
+ 8. **ToolAnnotationAssessor** - Policy #17 compliance
265
+ - readOnlyHint/destructiveHint verification
266
+ - Tool behavior inference from name patterns
267
+ - Annotation misalignment detection
268
+ - Policy #17 compliance reporting
269
+
270
+ 9. **ProhibitedLibrariesAssessor** - Policy #28-30 compliance
271
+ - Financial library detection (Stripe, PayPal, Plaid, etc.)
272
+ - Media library detection (Sharp, FFmpeg, OpenCV, PIL)
273
+ - package.json and requirements.txt scanning
274
+ - Source code import analysis
275
+
276
+ 10. **ManifestValidationAssessor** - MCPB manifest compliance
277
+ - manifest_version 0.3 validation
278
+ - Required field verification (name, version, mcp_config)
279
+ - Icon presence check
280
+ - ${BUNDLE_ROOT} anti-pattern detection
281
+
282
+ 11. **PortabilityAssessor** - Cross-platform compatibility
283
+ - Hardcoded path detection (/Users/, /home/, C:\)
284
+ - Platform-specific code patterns
285
+ - ${\_\_dirname} usage validation
286
+
287
+ **Recent Updates**:
288
+
289
+ - (2025-12-07): Added 5 new MCP Directory compliance assessors with 83 tests
290
+ - (2025-10-05): Removed 2,707 lines of out-of-scope assessment modules to focus on core MCP validation requirements
253
291
 
254
292
  ### 6. Advanced Assessment Components
255
293
 
@@ -443,40 +481,45 @@ Our assessment capabilities are backed by a comprehensive test suite that valida
443
481
 
444
482
  **Test Coverage Summary**:
445
483
 
446
- - **582 passing tests** across all project modules (100% pass rate)
447
- - **208 assessment module tests** specifically created for validation of our enhancements
484
+ - **665 passing tests** across all project modules (100% pass rate)
485
+ - **291 assessment module tests** specifically created for validation of our enhancements
448
486
 
449
487
  #### Assessment Module Test Breakdown
450
488
 
451
- The assessment functionality is validated by **208 specialized tests** across 14 test files:
489
+ The assessment functionality is validated by **291 specialized tests** across 19 test files:
452
490
 
453
491
  | Test File | Tests | Purpose |
454
492
  | ------------------------------------- | ------- | -------------------------------- |
455
493
  | `assessmentService.test.ts` | 54 | Comprehensive integration tests |
494
+ | `AUPComplianceAssessor.test.ts` | 26 | AUP policy violation detection |
495
+ | `ManifestValidationAssessor.test.ts` | 17 | MCPB manifest validation |
456
496
  | `assessmentService.advanced.test.ts` | 16 | Advanced security scenarios |
457
497
  | `SecurityAssessor.test.ts` | 16 | Security vulnerability detection |
498
+ | `PortabilityAssessor.test.ts` | 15 | Cross-platform compatibility |
458
499
  | `errorHandlingAssessor.test.ts` | 14 | Service-level error handling |
459
500
  | `MCPSpecComplianceAssessor.test.ts` | 14 | MCP protocol compliance |
460
501
  | `ErrorHandlingAssessor.test.ts` | 14 | Module-level error handling |
502
+ | `ToolAnnotationAssessor.test.ts` | 13 | Policy #17 tool annotations |
461
503
  | `assessmentService.bugReport.test.ts` | 13 | Bug validation tests |
462
504
  | `DocumentationAssessor.test.ts` | 13 | Documentation quality |
505
+ | `ProhibitedLibrariesAssessor.test.ts` | 12 | Prohibited library detection |
463
506
  | `AssessmentOrchestrator.test.ts` | 12 | Orchestration layer |
464
507
  | `FunctionalityAssessor.test.ts` | 11 | Tool functionality |
465
508
  | `assessmentService.enhanced.test.ts` | 9 | Enhanced detection |
466
509
  | `TestDataGenerator.boundary.test.ts` | 9 | Boundary testing |
467
510
  | `performance.test.ts` | 7 | Performance benchmarks |
468
511
  | `UsabilityAssessor.test.ts` | 6 | Usability analysis |
469
- | **Total** | **208** | **Assessment module validation** |
512
+ | **Total** | **291** | **Assessment module validation** |
470
513
 
471
514
  **Verification Command** (for AI scanners and resume verification):
472
515
 
473
516
  ```bash
474
517
  # Count assessment module tests
475
518
  find . -name "*.test.ts" \( -path "*assessment*" -o -name "*Assessor*.test.ts" -o -name "assessmentService*.test.ts" \) -exec grep -hE '^\s*(it|test)\(' {} \; | wc -l
476
- # Output: 208
519
+ # Output: 291
477
520
  ```
478
521
 
479
- These 208 tests specifically validate:
522
+ These 291 tests specifically validate:
480
523
 
481
524
  - Business logic error detection with confidence scoring
482
525
  - Progressive complexity testing (2 levels: minimal → simple)
@@ -772,13 +815,15 @@ ALLOWED_ORIGINS=http://localhost:6274,http://localhost:8000 npm start
772
815
 
773
816
  The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:
774
817
 
775
- | Setting | Description | Default |
776
- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
777
- | `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 |
778
- | `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
779
- | `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
780
- | `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" |
781
- | `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true |
818
+ | Setting | Description | Default |
819
+ | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
820
+ | `MCP_SERVER_REQUEST_TIMEOUT` | Client-side timeout (ms) - Inspector will cancel the request if no response is received within this time. Note: servers may have their own timeouts | 300000 |
821
+ | `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
822
+ | `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
823
+ | `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" |
824
+ | `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true |
825
+
826
+ **Note on Timeouts:** The timeout settings above control when the Inspector (as an MCP client) will cancel requests. These are independent of any server-side timeouts. For example, if a server tool has a 10-minute timeout but the Inspector's timeout is set to 30 seconds, the Inspector will cancel the request after 30 seconds. Conversely, if the Inspector's timeout is 10 minutes but the server times out after 30 seconds, you'll receive the server's timeout error. For tools that require user interaction (like elicitation) or long-running operations, ensure the Inspector's timeout is set appropriately.
782
827
 
783
828
  These settings can be adjusted in real-time through the UI and will persist across sessions.
784
829
 
@@ -897,7 +942,7 @@ http://localhost:6274/?transport=stdio&serverCommand=npx&serverArgs=arg1%20arg2
897
942
  You can also set initial config settings via query params, for example:
898
943
 
899
944
  ```
900
- http://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=10000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577
945
+ http://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=60000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577
901
946
  ```
902
947
 
903
948
  Note that if both the query param and the corresponding localStorage item are set, the query param will take precedence.
@@ -1051,6 +1096,17 @@ See [docs/mcp_vulnerability_testbed.md](docs/mcp_vulnerability_testbed.md) for d
1051
1096
  | **Automation** | N/A | Ideal for CI/CD pipelines, batch processing, and integration with coding assistants |
1052
1097
  | **Learning MCP** | Rich visual interface helps new users understand server capabilities | Simplified commands for focused learning of specific endpoints |
1053
1098
 
1099
+ ## Tool Input Validation Guidelines
1100
+
1101
+ When implementing or modifying tool input parameter handling in the Inspector:
1102
+
1103
+ - **Omit optional fields with empty values** - When processing form inputs, omit empty strings or null values for optional parameters, UNLESS the field has an explicit default value in the schema that matches the current value
1104
+ - **Preserve explicit default values** - If a field schema contains an explicit default (e.g., `default: null`), and the current value matches that default, include it in the request. This is a meaningful value the tool expects
1105
+ - **Always include required fields** - Preserve required field values even when empty, allowing the MCP server to validate and return appropriate error messages
1106
+ - **Defer deep validation to the server** - Implement basic field presence checking in the Inspector client, but rely on the MCP server for parameter validation according to its schema
1107
+
1108
+ These guidelines maintain clean parameter passing and proper separation of concerns between the Inspector client and MCP servers.
1109
+
1054
1110
  ## Evidence & Validation
1055
1111
 
1056
1112
  All performance claims in this README are backed by implementation analysis and documented methodology. We maintain transparency about what has been measured versus estimated.
@@ -1,7 +1,8 @@
1
1
  // List available prompts
2
- export async function listPrompts(client) {
2
+ export async function listPrompts(client, metadata) {
3
3
  try {
4
- const response = await client.listPrompts();
4
+ const params = metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {};
5
+ const response = await client.listPrompts(params);
5
6
  return response;
6
7
  }
7
8
  catch (error) {
@@ -9,7 +10,7 @@ export async function listPrompts(client) {
9
10
  }
10
11
  }
11
12
  // Get a prompt
12
- export async function getPrompt(client, name, args) {
13
+ export async function getPrompt(client, name, args, metadata) {
13
14
  try {
14
15
  // Convert all arguments to strings for prompt arguments
15
16
  const stringArgs = {};
@@ -26,10 +27,14 @@ export async function getPrompt(client, name, args) {
26
27
  }
27
28
  }
28
29
  }
29
- const response = await client.getPrompt({
30
+ const params = {
30
31
  name,
31
32
  arguments: stringArgs,
32
- });
33
+ };
34
+ if (metadata && Object.keys(metadata).length > 0) {
35
+ params._meta = metadata;
36
+ }
37
+ const response = await client.getPrompt(params);
33
38
  return response;
34
39
  }
35
40
  catch (error) {
@@ -1,7 +1,8 @@
1
1
  // List available resources
2
- export async function listResources(client) {
2
+ export async function listResources(client, metadata) {
3
3
  try {
4
- const response = await client.listResources();
4
+ const params = metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {};
5
+ const response = await client.listResources(params);
5
6
  return response;
6
7
  }
7
8
  catch (error) {
@@ -9,9 +10,13 @@ export async function listResources(client) {
9
10
  }
10
11
  }
11
12
  // Read a resource
12
- export async function readResource(client, uri) {
13
+ export async function readResource(client, uri, metadata) {
13
14
  try {
14
- const response = await client.readResource({ uri });
15
+ const params = { uri };
16
+ if (metadata && Object.keys(metadata).length > 0) {
17
+ params._meta = metadata;
18
+ }
19
+ const response = await client.readResource(params);
15
20
  return response;
16
21
  }
17
22
  catch (error) {
@@ -19,9 +24,10 @@ export async function readResource(client, uri) {
19
24
  }
20
25
  }
21
26
  // List resource templates
22
- export async function listResourceTemplates(client) {
27
+ export async function listResourceTemplates(client, metadata) {
23
28
  try {
24
- const response = await client.listResourceTemplates();
29
+ const params = metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {};
30
+ const response = await client.listResourceTemplates(params);
25
31
  return response;
26
32
  }
27
33
  catch (error) {
@@ -1,6 +1,7 @@
1
- export async function listTools(client) {
1
+ export async function listTools(client, metadata) {
2
2
  try {
3
- const response = await client.listTools();
3
+ const params = metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {};
4
+ const response = await client.listTools(params);
4
5
  return response;
5
6
  }
6
7
  catch (error) {
@@ -42,9 +43,9 @@ function convertParameters(tool, params) {
42
43
  }
43
44
  return result;
44
45
  }
45
- export async function callTool(client, name, args) {
46
+ export async function callTool(client, name, args, generalMetadata, toolSpecificMetadata) {
46
47
  try {
47
- const toolsResponse = await listTools(client);
48
+ const toolsResponse = await listTools(client, generalMetadata);
48
49
  const tools = toolsResponse.tools;
49
50
  const tool = tools.find((t) => t.name === name);
50
51
  let convertedArgs = args;
@@ -62,9 +63,21 @@ export async function callTool(client, name, args) {
62
63
  convertedArgs = { ...args, ...convertedStringArgs };
63
64
  }
64
65
  }
66
+ // Merge general metadata with tool-specific metadata
67
+ // Tool-specific metadata takes precedence over general metadata
68
+ let mergedMetadata;
69
+ if (generalMetadata || toolSpecificMetadata) {
70
+ mergedMetadata = {
71
+ ...(generalMetadata || {}),
72
+ ...(toolSpecificMetadata || {}),
73
+ };
74
+ }
65
75
  const response = await client.callTool({
66
76
  name: name,
67
77
  arguments: convertedArgs,
78
+ _meta: mergedMetadata && Object.keys(mergedMetadata).length > 0
79
+ ? mergedMetadata
80
+ : undefined,
68
81
  });
69
82
  return response;
70
83
  }
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import * as fs from "fs";
2
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
4
  import { Command } from "commander";
4
5
  import { callTool, connect, disconnect, getPrompt, listPrompts, listResources, listResourceTemplates, listTools, readResource, setLoggingLevel, validLogLevels, } from "./client/index.js";
5
6
  import { handleError } from "./error-handler.js";
6
7
  import { createTransport } from "./transport.js";
7
8
  import { awaitableLog } from "./utils/awaitable-log.js";
8
- import packageJson from "../package.json" with { type: "json" };
9
9
  function createTransportOptions(target, transport, headers) {
10
10
  if (target.length === 0) {
11
11
  throw new Error("Target is required. Specify a URL or a command to execute.");
@@ -52,6 +52,14 @@ function createTransportOptions(target, transport, headers) {
52
52
  };
53
53
  }
54
54
  async function callMethod(args) {
55
+ // Read package.json to get name and version for client identity
56
+ const pathA = "../package.json"; // We're in package @modelcontextprotocol/inspector-cli
57
+ const pathB = "../../package.json"; // We're in package @modelcontextprotocol/inspector
58
+ let packageJson;
59
+ let packageJsonData = await import(fs.existsSync(pathA) ? pathA : pathB, {
60
+ with: { type: "json" },
61
+ });
62
+ packageJson = packageJsonData.default;
55
63
  const transportOptions = createTransportOptions(args.target, args.transport, args.headers);
56
64
  const transport = createTransport(transportOptions);
57
65
  const [, name = packageJson.name] = packageJson.name.split("/");
@@ -63,36 +71,36 @@ async function callMethod(args) {
63
71
  let result;
64
72
  // Tools methods
65
73
  if (args.method === "tools/list") {
66
- result = await listTools(client);
74
+ result = await listTools(client, args.metadata);
67
75
  }
68
76
  else if (args.method === "tools/call") {
69
77
  if (!args.toolName) {
70
78
  throw new Error("Tool name is required for tools/call method. Use --tool-name to specify the tool name.");
71
79
  }
72
- result = await callTool(client, args.toolName, args.toolArg || {});
80
+ result = await callTool(client, args.toolName, args.toolArg || {}, args.metadata, args.toolMeta);
73
81
  }
74
82
  // Resources methods
75
83
  else if (args.method === "resources/list") {
76
- result = await listResources(client);
84
+ result = await listResources(client, args.metadata);
77
85
  }
78
86
  else if (args.method === "resources/read") {
79
87
  if (!args.uri) {
80
88
  throw new Error("URI is required for resources/read method. Use --uri to specify the resource URI.");
81
89
  }
82
- result = await readResource(client, args.uri);
90
+ result = await readResource(client, args.uri, args.metadata);
83
91
  }
84
92
  else if (args.method === "resources/templates/list") {
85
- result = await listResourceTemplates(client);
93
+ result = await listResourceTemplates(client, args.metadata);
86
94
  }
87
95
  // Prompts methods
88
96
  else if (args.method === "prompts/list") {
89
- result = await listPrompts(client);
97
+ result = await listPrompts(client, args.metadata);
90
98
  }
91
99
  else if (args.method === "prompts/get") {
92
100
  if (!args.promptName) {
93
101
  throw new Error("Prompt name is required for prompts/get method. Use --prompt-name to specify the prompt name.");
94
102
  }
95
- result = await getPrompt(client, args.promptName, args.promptArgs || {});
103
+ result = await getPrompt(client, args.promptName, args.promptArgs || {}, args.metadata);
96
104
  }
97
105
  // Logging methods
98
106
  else if (args.method === "logging/setLevel") {
@@ -199,7 +207,12 @@ function parseArgs() {
199
207
  //
200
208
  // HTTP headers
201
209
  //
202
- .option("--header <headers...>", 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', parseHeaderPair, {});
210
+ .option("--header <headers...>", 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', parseHeaderPair, {})
211
+ //
212
+ // Metadata options
213
+ //
214
+ .option("--metadata <pairs...>", "General metadata as key=value pairs (applied to all methods)", parseKeyValuePair, {})
215
+ .option("--tool-metadata <pairs...>", "Tool-specific metadata as key=value pairs (for tools/call method only)", parseKeyValuePair, {});
203
216
  // Parse only the arguments before --
204
217
  program.parse(preArgs);
205
218
  const options = program.opts();
@@ -213,6 +226,18 @@ function parseArgs() {
213
226
  target: finalArgs,
214
227
  ...options,
215
228
  headers: options.header, // commander.js uses 'header' field, map to 'headers'
229
+ metadata: options.metadata
230
+ ? Object.fromEntries(Object.entries(options.metadata).map(([key, value]) => [
231
+ key,
232
+ String(value),
233
+ ]))
234
+ : undefined,
235
+ toolMeta: options.toolMetadata
236
+ ? Object.fromEntries(Object.entries(options.toolMetadata).map(([key, value]) => [
237
+ key,
238
+ String(value),
239
+ ]))
240
+ : undefined,
216
241
  };
217
242
  }
218
243
  async function main() {
@@ -1,4 +1,4 @@
1
- import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-CynAt5P-.js";
1
+ import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-BwAoxcvr.js";
2
2
  const OAuthCallback = ({ onConnect }) => {
3
3
  const { toast } = useToast();
4
4
  const hasProcessedRef = reactExports.useRef(false);
@@ -1,4 +1,4 @@
1
- import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-CynAt5P-.js";
1
+ import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-BwAoxcvr.js";
2
2
  const OAuthDebugCallback = ({ onConnect }) => {
3
3
  reactExports.useEffect(() => {
4
4
  let isProcessed = false;
@@ -945,6 +945,12 @@ video {
945
945
  .mt-4 {
946
946
  margin-top: 1rem;
947
947
  }
948
+ .line-clamp-2 {
949
+ overflow: hidden;
950
+ display: -webkit-box;
951
+ -webkit-box-orient: vertical;
952
+ -webkit-line-clamp: 2;
953
+ }
948
954
  .block {
949
955
  display: block;
950
956
  }
@@ -1039,6 +1045,9 @@ video {
1039
1045
  .max-h-40 {
1040
1046
  max-height: 10rem;
1041
1047
  }
1048
+ .max-h-48 {
1049
+ max-height: 12rem;
1050
+ }
1042
1051
  .max-h-64 {
1043
1052
  max-height: 16rem;
1044
1053
  }
@@ -1646,6 +1655,10 @@ video {
1646
1655
  --tw-bg-opacity: 1;
1647
1656
  background-color: rgb(234 179 8 / var(--tw-bg-opacity, 1));
1648
1657
  }
1658
+ .object-contain {
1659
+ -o-object-fit: contain;
1660
+ object-fit: contain;
1661
+ }
1649
1662
  .p-0 {
1650
1663
  padding: 0px;
1651
1664
  }
@@ -1730,6 +1743,10 @@ video {
1730
1743
  padding-top: 1.5rem;
1731
1744
  padding-bottom: 1.5rem;
1732
1745
  }
1746
+ .py-8 {
1747
+ padding-top: 2rem;
1748
+ padding-bottom: 2rem;
1749
+ }
1733
1750
  .pb-1 {
1734
1751
  padding-bottom: 0.25rem;
1735
1752
  }
@@ -2441,6 +2458,11 @@ h1 {
2441
2458
  --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
2442
2459
  }
2443
2460
 
2461
+ .focus-visible\:ring-red-500:focus-visible {
2462
+ --tw-ring-opacity: 1;
2463
+ --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1));
2464
+ }
2465
+
2444
2466
  .focus-visible\:ring-ring:focus-visible {
2445
2467
  --tw-ring-color: hsl(var(--ring));
2446
2468
  }
@@ -2911,6 +2933,11 @@ h1 {
2911
2933
  color: rgb(243 244 246 / var(--tw-text-opacity, 1));
2912
2934
  }
2913
2935
 
2936
+ .dark\:text-gray-200:is(.dark *) {
2937
+ --tw-text-opacity: 1;
2938
+ color: rgb(229 231 235 / var(--tw-text-opacity, 1));
2939
+ }
2940
+
2914
2941
  .dark\:text-gray-300:is(.dark *) {
2915
2942
  --tw-text-opacity: 1;
2916
2943
  color: rgb(209 213 219 / var(--tw-text-opacity, 1));