@wix/mcp 1.0.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/build/index.js ADDED
@@ -0,0 +1,109 @@
1
+ import './sentry.js';
2
+ import minimist from 'minimist';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { addDocsTools, VALID_DOCS_TOOLS } from './docs/docs.js';
5
+ import { addCliTools, VALID_CLI_TOOLS } from './cli-tools/cli.js';
6
+ import { logger, attachStdErrLogger, attachMcpServerLogger, attachFileLogger } from './logger.js';
7
+ import { WixMcpServer } from './wix-mcp-server.js';
8
+ import { addDocsResources } from './resources/docs.js';
9
+ const VALID_TOOLS = [
10
+ 'WDS',
11
+ 'REST',
12
+ 'SDK',
13
+ 'BUILD_APPS',
14
+ 'WIX_HEADLESS'
15
+ ];
16
+ const EXPERIMENTAL_TOOLS = ['WIX_API', 'CLI_COMMAND'];
17
+ export const DEFAULT_TOOLS = [
18
+ 'WDS',
19
+ 'REST',
20
+ 'SDK',
21
+ 'BUILD_APPS',
22
+ 'WIX_HEADLESS'
23
+ ];
24
+ const parsedArgs = minimist(process.argv.slice(2));
25
+ function parseExperimentalArg() {
26
+ const experimentalArg = parsedArgs['experimental'];
27
+ if (!experimentalArg)
28
+ return [];
29
+ return experimentalArg
30
+ .split(',')
31
+ .map((t) => t.trim())
32
+ .filter((tool) => EXPERIMENTAL_TOOLS.includes(tool));
33
+ }
34
+ function parseToolsArg() {
35
+ const toolsArg = parsedArgs['tools'];
36
+ const experimentalTools = parseExperimentalArg();
37
+ if (!toolsArg) {
38
+ // When no tools specified, return both default and experimental tools
39
+ return [...DEFAULT_TOOLS, ...experimentalTools];
40
+ }
41
+ const requestedTools = toolsArg.split(',').map((t) => t.trim());
42
+ const tools = [
43
+ // Include valid non-experimental tools
44
+ ...requestedTools.filter((tool) => VALID_TOOLS.includes(tool)),
45
+ // Include enabled experimental tools
46
+ ...experimentalTools
47
+ ];
48
+ // Warn about enabled experimental tools
49
+ tools.forEach((tool) => {
50
+ if (EXPERIMENTAL_TOOLS.includes(tool)) {
51
+ logger.log(`Warning: ${tool} is an experimental tool and may have limited functionality or breaking changes`);
52
+ }
53
+ });
54
+ return tools;
55
+ }
56
+ const loggerType = parsedArgs['logger'] || 'mcp';
57
+ if (loggerType === 'file') {
58
+ attachFileLogger();
59
+ }
60
+ else {
61
+ // Initially we log to stderr, because MCP server is not connected yet
62
+ // When the server is connected, we attach the MCP server logger
63
+ attachStdErrLogger();
64
+ }
65
+ logger.log('--------------------------------');
66
+ logger.log('starting WIX MCP server');
67
+ logger.log('--------------------------------');
68
+ const server = new WixMcpServer({
69
+ name: 'wix-mcp-server',
70
+ version: '1.0.0'
71
+ }, {
72
+ capabilities: {
73
+ tools: {},
74
+ prompts: {},
75
+ logging: {},
76
+ resources: {}
77
+ }
78
+ });
79
+ const activeTools = parseToolsArg();
80
+ logger.log('Active tools:', activeTools);
81
+ const docsTools = activeTools.filter((tool) => VALID_DOCS_TOOLS.includes(tool));
82
+ if (docsTools.length > 0) {
83
+ logger.log('Adding docs tools:', docsTools);
84
+ addDocsTools(server, docsTools);
85
+ }
86
+ const cliTools = activeTools.filter((tool) => VALID_CLI_TOOLS.includes(tool));
87
+ if (cliTools.length > 0) {
88
+ logger.log('Adding cli tools:', cliTools);
89
+ addCliTools(server, cliTools);
90
+ }
91
+ try {
92
+ const portals = parsedArgs['portals']?.split(',') || [];
93
+ if (portals.length > 0) {
94
+ logger.log('Adding docs resources for portals:', portals);
95
+ await addDocsResources(server, portals);
96
+ }
97
+ }
98
+ catch (error) {
99
+ logger.error('Error adding docs resources:', error);
100
+ }
101
+ logger.log('Starting server');
102
+ const transport = new StdioServerTransport();
103
+ logger.log('Connecting to transport');
104
+ await server.connect(transport);
105
+ logger.log('Transport connected');
106
+ if (loggerType === 'mcp') {
107
+ // From now on, we log to the MCP server
108
+ attachMcpServerLogger(server);
109
+ }
@@ -0,0 +1,15 @@
1
+ import waitForExpect from 'wait-for-expect';
2
+ const defaultOptions = {
3
+ timeout: 10000,
4
+ interval: 200
5
+ };
6
+ export default async function eventually(expectation, options) {
7
+ return new Promise((resolve, reject) => {
8
+ waitForExpect
9
+ .default(async () => {
10
+ const ret = await expectation();
11
+ resolve(ret);
12
+ }, options?.timeout ?? defaultOptions.timeout, options?.interval ?? defaultOptions.interval)
13
+ .catch((error) => reject(error));
14
+ });
15
+ }
@@ -0,0 +1,75 @@
1
+ import stripAnsi from 'strip-ansi';
2
+ import eventually from './eventually.js';
3
+ import { logger } from '../logger.js';
4
+ export function handleStdout(stdout) {
5
+ let checkStartIndex = 0;
6
+ const lines = [];
7
+ logger.log(`handleStdout: stdout: ${stdout}`);
8
+ stdout.on('data', (chunk) => {
9
+ logger.log(`handleStdout: chunk: ${chunk}`);
10
+ // log(`create-cli: chunk: ${chunk}`);
11
+ lines.push(...stripAnsi(chunk.toString()).split('\n'));
12
+ });
13
+ return {
14
+ async waitForText(match, { timeout = 2 * 60 * 1000 } = {}) {
15
+ return eventually(() => {
16
+ const matchedIndex = lines.findIndex((line, index) => {
17
+ if (index < checkStartIndex) {
18
+ return false;
19
+ }
20
+ if (typeof match === 'string') {
21
+ return line.includes(match);
22
+ }
23
+ return line.match(match);
24
+ });
25
+ checkStartIndex = matchedIndex >= 0 ? matchedIndex + 1 : lines.length;
26
+ if (matchedIndex !== -1) {
27
+ return lines[matchedIndex];
28
+ }
29
+ const message = `Could not match text in output: ${match}`;
30
+ throw new Error(message);
31
+ }, { timeout });
32
+ },
33
+ async waitForAndCallback(matchesAndCallbacks, { timeout = 2 * 60 * 1000 } = {}) {
34
+ let stopResolve;
35
+ const stopPromise = new Promise((resolve) => {
36
+ stopResolve = resolve;
37
+ });
38
+ // Keep track of matches we've already processed
39
+ const processedMatches = new Set();
40
+ const waitPromise = eventually(() => {
41
+ //log(`create-cli: waiting for ${matchesAndCallbacks.length - processedMatches.size} matches`);
42
+ for (const { match, callback } of matchesAndCallbacks) {
43
+ // Skip if we've already processed this match
44
+ if (processedMatches.has(match)) {
45
+ continue;
46
+ }
47
+ if (lines.some((line) => line.includes(match))) {
48
+ logger.log(`create-cli: found match ${match}`);
49
+ // Add to processed matches so we don't match it again
50
+ processedMatches.add(match);
51
+ callback(() => {
52
+ logger.log(`create-cli: stopping match loop`);
53
+ stopResolve();
54
+ });
55
+ }
56
+ else {
57
+ // log(`create-cli: waiting for ${match}`);
58
+ }
59
+ }
60
+ // If we've processed all matches, we're done
61
+ if (processedMatches.size === matchesAndCallbacks.length) {
62
+ return true;
63
+ }
64
+ throw new Error(`Could not find any of the remaining matches: ${matchesAndCallbacks
65
+ .filter((m) => !processedMatches.has(m.match))
66
+ .map((m) => m.match)
67
+ .join(', ')}`);
68
+ }, { timeout });
69
+ return Promise.race([waitPromise, stopPromise]);
70
+ },
71
+ async getOutput() {
72
+ return lines.join('\n');
73
+ }
74
+ };
75
+ }
@@ -0,0 +1,75 @@
1
+ import { handleStdout } from './handleStdout.js';
2
+ import { logger } from '../logger.js';
3
+ export const ENTER = '\r';
4
+ const DOWN = '\u001b[B';
5
+ export const mockInteractiveGenerateCommandTool = async ({ extensionType, stdin, stdout }) => {
6
+ try {
7
+ logger.log('RunWixCliCommand: handle stdout');
8
+ const stdoutHandler = handleStdout(stdout);
9
+ logger.log('RunWixCliCommand: waiting for text');
10
+ await stdoutHandler.waitForText('What kind of extension would you like to generate?');
11
+ // Enter the extension type
12
+ if (extensionType === 'DASHBOARD_PAGE') {
13
+ // No need to press enter
14
+ }
15
+ else if (extensionType === 'BACKEND_EVENT') {
16
+ logger.log('RunWixCliCommand: writing DOWN 9 times');
17
+ for (let i = 0; i < 9; i++) {
18
+ stdin.write(DOWN);
19
+ await new Promise((resolve) => setTimeout(resolve, 10));
20
+ }
21
+ }
22
+ logger.log('RunWixCliCommand: writing ENTER 1');
23
+ stdin.write(ENTER);
24
+ // Wait for next step
25
+ if (extensionType === 'DASHBOARD_PAGE') {
26
+ await stdoutHandler.waitForText('Page title');
27
+ logger.log('RunWixCliCommand: writing ENTER 2');
28
+ stdin.write(ENTER);
29
+ await stdoutHandler.waitForText('Enter the route for the new page');
30
+ logger.log('RunWixCliCommand: writing ENTER 3');
31
+ stdin.write(ENTER);
32
+ }
33
+ else if (extensionType === 'BACKEND_EVENT') {
34
+ await stdoutHandler.waitForText('Event folder');
35
+ logger.log('RunWixCliCommand: writing ENTER 2');
36
+ stdin.write(ENTER);
37
+ logger.log('RunWixCliCommand: waiting for text');
38
+ // Check if the text is "Would you like to install dependencies now?", if dependencies are already installed, it will not be printed
39
+ try {
40
+ await stdoutHandler.waitForText('Would you like to install dependencies now?', { timeout: 1000 });
41
+ logger.log('RunWixCliCommand: writing ENTER 3');
42
+ stdin.write(ENTER);
43
+ }
44
+ catch (error) {
45
+ if (error instanceof Error &&
46
+ error.message.includes('Could not match text in output')) {
47
+ logger.log('RunWixCliCommand: dependencies are already installed, skipping install');
48
+ logger.log(await stdoutHandler.getOutput());
49
+ }
50
+ else {
51
+ throw error;
52
+ }
53
+ }
54
+ }
55
+ logger.log('RunWixCliCommand: waiting for success message');
56
+ try {
57
+ await stdoutHandler.waitForText('Successfully', { timeout: 1000 });
58
+ logger.log('RunWixCliCommand: success message found');
59
+ }
60
+ catch (error) {
61
+ if (error instanceof Error &&
62
+ error.message.includes('Could not match text in output')) {
63
+ logger.log('RunWixCliCommand: success message not found, skipping'); // just skip and try to continue by returning the output
64
+ }
65
+ else {
66
+ throw error;
67
+ }
68
+ }
69
+ return stdoutHandler.getOutput();
70
+ }
71
+ catch (error) {
72
+ logger.error('RunWixCliCommand: error', error);
73
+ throw error;
74
+ }
75
+ };
@@ -0,0 +1,61 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { homedir } from 'os';
4
+ const createNullLogger = () => {
5
+ return {
6
+ log: () => { },
7
+ error: () => { }
8
+ };
9
+ };
10
+ const createStdErrLogger = () => {
11
+ return {
12
+ log: (data) => {
13
+ console.error(data);
14
+ },
15
+ error: (data) => {
16
+ console.error(data);
17
+ }
18
+ };
19
+ };
20
+ const createMcpServerLogger = (server) => {
21
+ return {
22
+ log: (data) => {
23
+ server.server.sendLoggingMessage({ level: 'info', data });
24
+ },
25
+ error: (data) => {
26
+ server.server.sendLoggingMessage({ level: 'error', data });
27
+ }
28
+ };
29
+ };
30
+ const createFileLogger = () => {
31
+ const filePath = path.join(homedir(), 'wix-mcp-log.txt');
32
+ const date = new Date()
33
+ .toLocaleString('en-US', { timeZone: 'Asia/Jerusalem' })
34
+ .replace(/,/g, '');
35
+ return {
36
+ log: (...data) => {
37
+ const message = `[info] ${date} - ${data.join(' ')}`;
38
+ fs.appendFile(filePath, `${message}\n`);
39
+ },
40
+ error: (...data) => {
41
+ const message = `[error] ${date} - ${data.join(' ')}`;
42
+ fs.appendFile(filePath, `${message}\n`);
43
+ }
44
+ };
45
+ };
46
+ export const logger = createNullLogger();
47
+ export const attachMcpServerLogger = (server) => {
48
+ const mcpLogger = createMcpServerLogger(server);
49
+ logger.log = mcpLogger.log;
50
+ logger.error = mcpLogger.error;
51
+ };
52
+ export const attachStdErrLogger = () => {
53
+ const stdErrLogger = createStdErrLogger();
54
+ logger.log = stdErrLogger.log;
55
+ logger.error = stdErrLogger.error;
56
+ };
57
+ export const attachFileLogger = () => {
58
+ const fileLogger = createFileLogger();
59
+ logger.log = fileLogger.log;
60
+ logger.error = fileLogger.error;
61
+ };
@@ -0,0 +1,18 @@
1
+ import { createRequire } from 'module';
2
+ import { createGlobalConfig, panoramaClientFactory, PanoramaPlatform } from '@wix/panorama-client-node';
3
+ const require = createRequire(import.meta.url);
4
+ const packageJson = require('../package.json');
5
+ export const globalConfig = createGlobalConfig();
6
+ export const panoramaFactory = panoramaClientFactory({
7
+ baseParams: {
8
+ platform: PanoramaPlatform.Standalone,
9
+ fullArtifactId: 'com.wixpress.spartans.wix-mcp',
10
+ artifactVersion: packageJson.version
11
+ },
12
+ data: {
13
+ packageVersion: packageJson.version,
14
+ versions: process.versions,
15
+ platform: process.platform,
16
+ arch: process.arch
17
+ }
18
+ }).withGlobalConfig(globalConfig);
@@ -0,0 +1,45 @@
1
+ import { logger } from '../logger.js';
2
+ const getPortalIndex = async (portalName) => {
3
+ const response = await fetch(`https://dev.wix.com/digor/api/portal-index?portalName=${portalName}`);
4
+ const data = await response.json();
5
+ return data;
6
+ };
7
+ const addPortalResources = async (server, portalName) => {
8
+ // get portal index:
9
+ const portalIndexResponse = await getPortalIndex(portalName);
10
+ logger.log(`portalIndexResponse for ${portalName}`, JSON.stringify(portalIndexResponse, null, 2));
11
+ // for each portalIndex entry, add a resource to the server:
12
+ for (const entry of portalIndexResponse.portalIndex) {
13
+ logger.log(`entry ${JSON.stringify(entry, null, 2)}`);
14
+ const name = entry.url;
15
+ const uri = entry.url.replace('https://dev.wix.com/docs/', 'wix-docs://');
16
+ logger.log(`adding resource ${name} ${uri}`);
17
+ server.resource(name, uri, async (uri) => {
18
+ logger.log(`fetching resource ${uri}`);
19
+ const docsURL = uri
20
+ .toString()
21
+ .replace('wix-docs://', 'https://dev.wix.com/docs/');
22
+ const response = await fetch(`https://dev.wix.com/digor/api/get-article-content?articleUrl=${encodeURIComponent(docsURL)}`);
23
+ const data = await response.json();
24
+ return {
25
+ contents: [
26
+ {
27
+ uri: uri.href,
28
+ text: data.articleContent || 'No content found'
29
+ }
30
+ ]
31
+ };
32
+ });
33
+ }
34
+ };
35
+ export const addDocsResources = async (server, portals) => {
36
+ for (const portal of portals) {
37
+ try {
38
+ logger.log(`Processing portal: ${portal}`);
39
+ await addPortalResources(server, portal);
40
+ }
41
+ catch (error) {
42
+ logger.error(`Error processing portal ${portal}:`, error);
43
+ }
44
+ }
45
+ };
@@ -0,0 +1,10 @@
1
+ import * as Sentry from '@sentry/node';
2
+ Sentry.init({
3
+ dsn: 'https://583c5af58c664fd1977d638a693b0ada@sentry-next.wixpress.com/20924',
4
+ tracesSampleRate: 1.0,
5
+ initialScope: {
6
+ tags: {
7
+ fullArtifactId: 'com.wixpress.spartans.wix-mcp'
8
+ }
9
+ }
10
+ });
@@ -0,0 +1,50 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { globalConfig, panoramaFactory } from './panorama.js';
3
+ import { captureException, setTags } from '@sentry/node';
4
+ export class WixMcpServer extends McpServer {
5
+ tool(...args) {
6
+ const cbIndex = args.findIndex((arg) => typeof arg === 'function');
7
+ if (cbIndex !== -1) {
8
+ const originalCb = args[cbIndex];
9
+ const toolName = args[0];
10
+ const panoramaComponentId = toolName;
11
+ const panorama = panoramaFactory.client({
12
+ baseParams: {
13
+ componentId: panoramaComponentId
14
+ }
15
+ });
16
+ setTags({
17
+ panoramaSessionId: globalConfig.getSessionId()
18
+ });
19
+ const wrappedCb = async (...cbArgs) => {
20
+ const argsBeforeExtra = cbArgs.slice(0, cbArgs.length - 1);
21
+ const extra = cbArgs[cbArgs.length - 1];
22
+ const wrappedExtra = {
23
+ ...extra,
24
+ panorama: panorama.createClientForComponent()
25
+ };
26
+ panorama.transaction(toolName).start();
27
+ try {
28
+ const cbResult = await originalCb(...argsBeforeExtra, wrappedExtra);
29
+ panorama.transaction(toolName).finish();
30
+ return cbResult;
31
+ }
32
+ catch (e) {
33
+ panorama.errorMonitor().reportError(e);
34
+ captureException(e, {
35
+ tags: {
36
+ componentId: panoramaComponentId,
37
+ toolName
38
+ }
39
+ });
40
+ return {
41
+ isError: true,
42
+ content: [{ type: 'text', text: e.message }]
43
+ };
44
+ }
45
+ };
46
+ args[cbIndex] = wrappedCb;
47
+ }
48
+ return super.tool.apply(this, args);
49
+ }
50
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@wix/mcp",
3
+ "version": "1.0.0",
4
+ "description": "A Model Context Protocol server for Wix AI tools",
5
+ "type": "module",
6
+ "bin": "./bin.js",
7
+ "files": [
8
+ "build"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "start": "npm run build && node ./build/index.js",
15
+ "watch": "tsc --watch",
16
+ "lint": "eslint .",
17
+ "inspector": "npx @modelcontextprotocol/inspector build/index.js"
18
+ },
19
+ "publishConfig": {
20
+ "registry": "https://registry.npmjs.org/",
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.9.0",
25
+ "@sentry/node": "^9.12.0",
26
+ "@wix/panorama-client-node": "^3.227.0",
27
+ "execa": "^9.5.2",
28
+ "minimist": "^1.2.8",
29
+ "strip-ansi": "^7.1.0",
30
+ "uuid": "^11.1.0",
31
+ "wait-for-expect": "^3.0.2",
32
+ "zod": "^3.24.2"
33
+ },
34
+ "devDependencies": {
35
+ "@eslint/js": "^9.24.0",
36
+ "@types/express": "^5.0.1",
37
+ "@types/minimist": "^1.2.5",
38
+ "@types/node": "^20.17.30",
39
+ "eslint": "^9.24.0",
40
+ "eslint-config-prettier": "^10.1.2",
41
+ "eslint-plugin-prettier": "^5.2.6",
42
+ "globals": "^16.0.0",
43
+ "prettier": "^3.5.3",
44
+ "typescript": "^5.8.3",
45
+ "typescript-eslint": "^8.30.1",
46
+ "vitest": "^3.1.1"
47
+ },
48
+ "exports": {
49
+ ".": {
50
+ "import": "./build/index.js"
51
+ },
52
+ "./package.json": "./package.json"
53
+ },
54
+ "wix": {
55
+ "artifact": {
56
+ "artifactId": "wix-mcp",
57
+ "groupId": "com.wixpress.spartans"
58
+ },
59
+ "validations": {
60
+ "source": [
61
+ "lint"
62
+ ]
63
+ }
64
+ },
65
+ "falconPackageHash": "bf847433e48397ca724ff50a4883a0a7a5aedc5c5cb6af9048001461"
66
+ }