@viewlint/mcp 0.0.1

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 ADDED
@@ -0,0 +1,19 @@
1
+ # @viewlint/mcp
2
+
3
+ MCP server for ViewLint. You can use ViewLint with any AI Agent to improve its UI design skills.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @viewlint/mcp@latest
9
+ ```
10
+
11
+ Or:
12
+
13
+ ```bash
14
+ viewlint --mcp
15
+ ```
16
+
17
+ ## What it does
18
+
19
+ See the [docs](https://viewlint.vercel.app/docs/mcp-server)
@@ -0,0 +1,2 @@
1
+ export { mcpServer } from "./mcp-server.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { mcpServer } from "./mcp-server.js";
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=mcp-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-cli.d.ts","sourceRoot":"","sources":["../src/mcp-cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { mcpServer } from "./mcp-server.js";
4
+ function disconnect() {
5
+ mcpServer.close();
6
+ process.exitCode = 0;
7
+ }
8
+ await mcpServer.connect(new StdioServerTransport());
9
+ // Note: do not use stdout because it's used for the MCP transport.
10
+ console.error(`ViewLint MCP server is running. cwd: ${process.cwd()}`);
11
+ process.on("SIGINT", disconnect);
12
+ process.on("SIGTERM", disconnect);
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const mcpServer: McpServer;
3
+ //# sourceMappingURL=mcp-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AA0EnE,eAAO,MAAM,SAAS,WAGpB,CAAA"}
@@ -0,0 +1,266 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ViewLint } from "viewlint";
3
+ import { defaultView, findNearestViewlintConfigFile } from "viewlint/config";
4
+ import { getConfigInputSchema, lintUrlsInputSchema } from "./types.js";
5
+ // Helper to count severities
6
+ function countSeverities(messages) {
7
+ let errorCount = 0;
8
+ let warningCount = 0;
9
+ let infoCount = 0;
10
+ for (const message of messages) {
11
+ if (message.severity === "error")
12
+ errorCount += 1;
13
+ if (message.severity === "warn")
14
+ warningCount += 1;
15
+ if (message.severity === "info")
16
+ infoCount += 1;
17
+ }
18
+ return { errorCount, warningCount, infoCount };
19
+ }
20
+ // Filter results for quiet mode (errors only)
21
+ function filterResultsForQuietMode(results) {
22
+ return results.map((result) => {
23
+ const messages = result.messages.filter((m) => m.severity === "error");
24
+ const counts = countSeverities(messages);
25
+ return {
26
+ ...result,
27
+ messages,
28
+ suppressedMessages: [],
29
+ errorCount: counts.errorCount,
30
+ warningCount: 0,
31
+ infoCount: 0,
32
+ recommendCount: 0,
33
+ };
34
+ });
35
+ }
36
+ // Format results for MCP tool response - always JSON
37
+ function formatToolResults(results) {
38
+ const type = "text";
39
+ const content = [];
40
+ // Summary statistics
41
+ const totalErrors = results.reduce((sum, r) => sum + r.errorCount, 0);
42
+ const totalWarnings = results.reduce((sum, r) => sum + r.warningCount, 0);
43
+ const totalInfo = results.reduce((sum, r) => sum + r.infoCount, 0);
44
+ content.push({
45
+ type,
46
+ text: `ViewLint Results Summary: ${totalErrors} error(s), ${totalWarnings} warning(s), ${totalInfo} info message(s) across ${results.length} URL(s).`,
47
+ });
48
+ // JSON format - structured for programmatic use
49
+ content.push({
50
+ type,
51
+ text: JSON.stringify(results, null, 2),
52
+ });
53
+ content.push({
54
+ type,
55
+ text: "\nReview the issues above. If you were asked to fix issues, proceed with fixing them. Otherwise, summarize the findings and ask the user if they want you to address any issues.",
56
+ });
57
+ return content;
58
+ }
59
+ // Initialize the MCP server
60
+ export const mcpServer = new McpServer({
61
+ name: "ViewLint",
62
+ version: "0.0.1",
63
+ });
64
+ // Main linting tool
65
+ mcpServer.registerTool("lint", {
66
+ title: "Lint URLs",
67
+ description: `Lint one or more web page URLs for accessibility and UI issues using ViewLint.
68
+
69
+ ViewLint requires a configuration file to be defined, in the form of \`viewlint.config.js|ts|mjs\`. This file defines rules, views, options, and scopes for linting. ViewLint will not be able to function correctly without a valid configuration file, and views, options, and scopes must be defined in the configuration file.
70
+
71
+ **IMPORTANT**: Before running this tool, use the 'get-config' tool to discover which config file is being used. If no config file exists, the 'get-config' tool will inform you that you can create one for the user.
72
+
73
+ # Basic Usage:
74
+ Just provide one or more URLs to lint. ViewLint will load each page and check for accessibility issues, UI problems, and best practice violations.
75
+
76
+ # Advanced Features (optional):
77
+ - view: Use a named view from config (defines page setup like authentication, navigation)
78
+ - options: Apply named option layers (viewport size, cookies, storage state)
79
+ - scopes: Limit linting to specific named page sections
80
+ - selectors: Limit linting to elements matching CSS selectors
81
+ - quiet: Show only errors, hide warnings`,
82
+ inputSchema: lintUrlsInputSchema,
83
+ }, async (input) => {
84
+ const { urls, configFile, view: viewName, options: optionNames, scopes: scopeNames, selectors, quiet = false, } = input;
85
+ try {
86
+ const viewlint = new ViewLint({
87
+ overrideConfigFile: configFile,
88
+ });
89
+ const resolved = await viewlint.getResolvedOptions();
90
+ // Resolve named options from config
91
+ const optionLayersFromRegistry = (optionNames ?? []).flatMap((name) => {
92
+ const entry = resolved.optionsRegistry.get(name);
93
+ if (!entry) {
94
+ const known = [...resolved.optionsRegistry.keys()].sort();
95
+ const knownMessage = known.length === 0
96
+ ? "No named options are defined in config."
97
+ : `Known options: ${known.map((x) => `'${x}'`).join(", ")}.`;
98
+ throw new Error(`Unknown option '${name}'. ${knownMessage}`);
99
+ }
100
+ const layers = Array.isArray(entry) ? entry : [entry];
101
+ return layers.map((layer) => {
102
+ if (layer.meta?.name)
103
+ return layer;
104
+ return { ...layer, meta: { ...(layer.meta ?? {}), name } };
105
+ });
106
+ });
107
+ // Resolve named scopes from config
108
+ const scopesFromRegistry = (scopeNames ?? []).flatMap((name) => {
109
+ const entry = resolved.scopeRegistry.get(name);
110
+ if (!entry) {
111
+ const known = [...resolved.scopeRegistry.keys()].sort();
112
+ const knownMessage = known.length === 0
113
+ ? "No named scopes are defined in config."
114
+ : `Known scopes: ${known.map((x) => `'${x}'`).join(", ")}.`;
115
+ throw new Error(`Unknown scope '${name}'. ${knownMessage}`);
116
+ }
117
+ const scopes = Array.isArray(entry) ? entry : [entry];
118
+ return scopes.map((scope) => {
119
+ if (scope.meta?.name)
120
+ return scope;
121
+ return { ...scope, meta: { ...(scope.meta ?? {}), name } };
122
+ });
123
+ });
124
+ // Create scopes from CSS selectors
125
+ const selectorScopes = (selectors ?? []).map((selector) => ({
126
+ meta: { name: selector },
127
+ getLocator: ({ page }) => page.locator(selector),
128
+ }));
129
+ const resolvedScopes = [...scopesFromRegistry, ...selectorScopes];
130
+ // Resolve view
131
+ const resolveView = () => {
132
+ if (viewName) {
133
+ const view = resolved.viewRegistry.get(viewName);
134
+ if (!view) {
135
+ const known = [...resolved.viewRegistry.keys()].sort();
136
+ const knownMessage = known.length === 0
137
+ ? "No named views are defined in config."
138
+ : `Known views: ${known.map((x) => `'${x}'`).join(", ")}.`;
139
+ throw new Error(`Unknown view '${viewName}'. ${knownMessage}`);
140
+ }
141
+ if (view.meta?.name)
142
+ return view;
143
+ return { ...view, meta: { ...(view.meta ?? {}), name: viewName } };
144
+ }
145
+ return defaultView;
146
+ };
147
+ // Build targets for each URL
148
+ const targets = urls.map((url) => {
149
+ const urlLayer = [{ context: { baseURL: url } }];
150
+ return {
151
+ view: resolveView(),
152
+ options: urlLayer.length === 0 && optionLayersFromRegistry.length === 0
153
+ ? undefined
154
+ : [...urlLayer, ...optionLayersFromRegistry],
155
+ scope: resolvedScopes.length === 0 ? undefined : resolvedScopes,
156
+ };
157
+ });
158
+ let results = await viewlint.lintTargets(targets);
159
+ // Apply quiet mode filter
160
+ if (quiet) {
161
+ results = filterResultsForQuietMode(results);
162
+ }
163
+ const totalErrors = results.reduce((sum, r) => sum + r.errorCount, 0);
164
+ const content = formatToolResults(results);
165
+ return {
166
+ content,
167
+ isError: totalErrors > 0,
168
+ };
169
+ }
170
+ catch (error) {
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: `Error running ViewLint: ${error instanceof Error ? error.message : String(error)}`,
176
+ },
177
+ ],
178
+ isError: true,
179
+ };
180
+ }
181
+ });
182
+ // Tool to get config file path
183
+ mcpServer.registerTool("get-config", {
184
+ title: "Get ViewLint Config",
185
+ description: `Get the path to the ViewLint configuration file currently being used.
186
+
187
+ This tool discovers and returns the path to the viewlint.config.ts|js|mjs file that ViewLint will use for linting. It searches from the server's working directory upward.
188
+
189
+ If no config file is found, the tool will inform you and you can offer to create one for the user.`,
190
+ inputSchema: getConfigInputSchema,
191
+ }, async (input) => {
192
+ const { configFile } = input;
193
+ try {
194
+ // If a specific config file was requested, check it
195
+ if (configFile) {
196
+ return {
197
+ content: [
198
+ {
199
+ type: "text",
200
+ text: `Specified config file: ${configFile}`,
201
+ },
202
+ ],
203
+ };
204
+ }
205
+ // Otherwise, discover the config file
206
+ const discoveredConfig = findNearestViewlintConfigFile();
207
+ if (discoveredConfig) {
208
+ return {
209
+ content: [
210
+ {
211
+ type: "text",
212
+ text: `ViewLint config file found: ${discoveredConfig}\n\nThis config file defines rules, views, options, and scopes for linting. You can now use the 'lint' tool to lint URLs using this configuration.`,
213
+ },
214
+ ],
215
+ };
216
+ }
217
+ // No config file found
218
+ return {
219
+ content: [
220
+ {
221
+ type: "text",
222
+ text: `No ViewLint config file found in the current working directory or any parent directory.
223
+
224
+ ViewLint requires a configuration file in one of these formats:
225
+ - viewlint.config.ts
226
+ - viewlint.config.js
227
+ - viewlint.config.mjs
228
+
229
+ You can offer to create a basic viewlint.config.ts file for the user. A minimal config looks like:
230
+
231
+ \`\`\`typescript
232
+ import { defineConfig } from "viewlint/config";
233
+
234
+ export default defineConfig({
235
+ rules: {
236
+ // Add rules here
237
+ },
238
+ views: {
239
+ // Define views here (e.g., mobile, desktop, logged-in)
240
+ },
241
+ options: {
242
+ // Define reusable option layers
243
+ },
244
+ scopes: {
245
+ // Define scope selectors here
246
+ },
247
+ });
248
+ \`\`\`
249
+
250
+ Alternatively, you can ask the user to run \`npm init @viewlint/config\` for an interactive guided setup.`,
251
+ },
252
+ ],
253
+ };
254
+ }
255
+ catch (error) {
256
+ return {
257
+ content: [
258
+ {
259
+ type: "text",
260
+ text: `Error discovering config: ${error instanceof Error ? error.message : String(error)}`,
261
+ },
262
+ ],
263
+ isError: true,
264
+ };
265
+ }
266
+ });
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ export declare const lintUrlsInputSchema: {
3
+ urls: z.ZodArray<z.ZodURL>;
4
+ configFile: z.ZodOptional<z.ZodString>;
5
+ view: z.ZodOptional<z.ZodString>;
6
+ options: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
8
+ selectors: z.ZodOptional<z.ZodArray<z.ZodString>>;
9
+ quiet: z.ZodOptional<z.ZodBoolean>;
10
+ };
11
+ export type LintUrlsInput = z.infer<z.ZodObject<typeof lintUrlsInputSchema>>;
12
+ export declare const getConfigInputSchema: {
13
+ configFile: z.ZodOptional<z.ZodString>;
14
+ };
15
+ export type GetConfigInput = z.infer<z.ZodObject<typeof getConfigInputSchema>>;
16
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,eAAO,MAAM,mBAAmB;;;;;;;;CA0C/B,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAA;AAG5E,eAAO,MAAM,oBAAoB;;CAQhC,CAAA;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ // Main lint-urls tool schema - simple by default, advanced options available
3
+ export const lintUrlsInputSchema = {
4
+ urls: z
5
+ .array(z.url())
6
+ .min(1)
7
+ .describe("One or more URLs to lint (e.g. https://example.com)"),
8
+ configFile: z
9
+ .string()
10
+ .min(1)
11
+ .optional()
12
+ .describe("Path to a specific viewlint config file. If omitted, uses the default config from the server's working directory. Use the 'get-config' tool to discover the config file being used."),
13
+ view: z
14
+ .string()
15
+ .optional()
16
+ .describe("Use a named view from the config. Views define how the page is loaded and set up before linting."),
17
+ options: z
18
+ .array(z.string())
19
+ .optional()
20
+ .describe("Apply named option layers from config (in order). Options can set viewport, authentication, or other context."),
21
+ scopes: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("Apply named scopes from config (in order). Scopes limit linting to specific parts of the page."),
25
+ selectors: z
26
+ .array(z.string())
27
+ .optional()
28
+ .describe("Ad-hoc CSS selectors to use as additional scope roots. Limits linting to elements matching these selectors."),
29
+ quiet: z
30
+ .boolean()
31
+ .optional()
32
+ .describe("If true, only report errors (hide warnings and info messages). Default: false."),
33
+ };
34
+ // Get config tool schema
35
+ export const getConfigInputSchema = {
36
+ configFile: z
37
+ .string()
38
+ .min(1)
39
+ .optional()
40
+ .describe("Optional path to check a specific config file. If omitted, discovers the config from the server's working directory."),
41
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@viewlint/mcp",
3
+ "version": "0.0.1",
4
+ "description": "MCP server for ViewLint",
5
+ "type": "module",
6
+ "bin": {
7
+ "viewlint-mcp": "dist/mcp-cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./mcp-server": {
15
+ "types": "./dist/mcp-server.d.ts",
16
+ "default": "./dist/mcp-server.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.24.1",
28
+ "viewlint": "^0.0.1",
29
+ "zod": "^4.3.5"
30
+ },
31
+ "devDependencies": {
32
+ "@repo/typescript-config": "workspace:*",
33
+ "typescript": "5.9.2",
34
+ "vitest": "3.2.4",
35
+ "viewlint": "workspace:*"
36
+ },
37
+ "scripts": {
38
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json && node -e \"require('node:fs').chmodSync('dist/mcp-cli.js', 0o755)\"",
39
+ "test": "vitest run --passWithNoTests",
40
+ "check-types": "tsc --noEmit",
41
+ "prepack": "npm run build"
42
+ }
43
+ }