@ompo-design/mcp-server 0.1.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 ADDED
@@ -0,0 +1,67 @@
1
+ # @ompo-design/mcp-server
2
+
3
+ Local MCP server for applying Ompo visual edits to a codebase.
4
+
5
+ ## Setup
6
+
7
+ ### Cursor
8
+
9
+ Add to `.cursor/mcp.json` in your project:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "ompo": {
15
+ "command": "npx",
16
+ "args": ["-y", "@ompo-design/mcp-server"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ ### Claude Code
23
+
24
+ Add to `.mcp.json` in your project:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "ompo": {
30
+ "type": "stdio",
31
+ "command": "npx",
32
+ "args": ["-y", "@ompo-design/mcp-server"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ Restart your IDE session after adding the config. In Claude Code, run `/mcp` to confirm `ompo` is connected with 4 tools.
39
+
40
+ ## Workflow
41
+
42
+ 1. Edit your localhost site in Ompo
43
+ 2. Click **Send** and copy the prompt
44
+ 3. Paste the prompt into Cursor or Claude Code
45
+ 4. The agent calls:
46
+ - `get_edit({ id: "ed_8K42P" })`
47
+ - `apply_edit({ id: "ed_8K42P" })`
48
+ 5. The agent applies only the listed property changes to source files
49
+
50
+ Ompo writes edit bundles to `.ompo/edits/` in your project. Open that project in your IDE so the MCP server can find them.
51
+
52
+ ## Tools
53
+
54
+ - `list_edits` — list saved edit bundles in `.ompo/edits/`
55
+ - `get_edit` — load a bundle by id
56
+ - `explain_edit` — short summary of an edit
57
+ - `apply_edit` — structured apply plan for the agent
58
+
59
+ ## Development
60
+
61
+ ```bash
62
+ npm install
63
+ npm run build
64
+ npm start
65
+ ```
66
+
67
+ For local testing before publish, point your MCP config at `node /absolute/path/to/mcp-server/dist/index.js`.
@@ -0,0 +1,29 @@
1
+ import type { OmpoEditBundle, OmpoOperation } from './types.js';
2
+ export type ApplySuggestion = {
3
+ property: string;
4
+ value: string;
5
+ strategy: 'inline-style' | 'css-rule' | 'component-markup' | 'text-content' | 'dom-structure';
6
+ notes?: string;
7
+ };
8
+ export type ApplyPlanFile = {
9
+ selector: string;
10
+ suggestions: ApplySuggestion[];
11
+ };
12
+ export type DomStructurePlan = {
13
+ kind: 'dom.insert' | 'dom.move' | 'dom.delete' | 'dom.flexWrap';
14
+ summary: string;
15
+ steps: string[];
16
+ payload: Record<string, unknown>;
17
+ };
18
+ export type ApplyPlan = {
19
+ editId: string;
20
+ scope: OmpoEditBundle['scope'];
21
+ sourceUrl: string;
22
+ files: ApplyPlanFile[];
23
+ domStructurePlans: DomStructurePlan[];
24
+ domChanges: OmpoOperation[];
25
+ warnings: string[];
26
+ instructions: string[];
27
+ };
28
+ export declare function buildApplyPlan(bundle: OmpoEditBundle): ApplyPlan;
29
+ export declare function explainEdit(bundle: OmpoEditBundle): string;
@@ -0,0 +1,172 @@
1
+ function styleSuggestions(operation) {
2
+ return Object.entries(operation.changed).map(([property, value]) => ({
3
+ property,
4
+ value: String(value),
5
+ strategy: 'inline-style',
6
+ notes: 'Map this changed property to the matching source stylesheet, class, or component prop. Do not modify unrelated styles.'
7
+ }));
8
+ }
9
+ function buildDomStructurePlan(operation) {
10
+ switch (operation.kind) {
11
+ case 'dom.flexWrap':
12
+ return {
13
+ kind: 'dom.flexWrap',
14
+ summary: `Wrap ${operation.children.length} element(s) in a new flex ${operation.flexDirection} container`,
15
+ steps: [
16
+ `Locate the parent element matching "${operation.parentSelector}" in source.`,
17
+ `Insert a new <div> wrapper at child index ${operation.index} inside that parent.`,
18
+ `Move the matched child elements into the new wrapper. Use the child anchors (tag, id, class, textSnippet) to find the correct nodes in source.`,
19
+ `Apply only the wrapper styles listed in wrapperStyles to the new container.`,
20
+ 'Do not change unrelated siblings or parent styling.'
21
+ ],
22
+ payload: {
23
+ wrapperSelector: operation.wrapperSelector,
24
+ parentSelector: operation.parentSelector,
25
+ insertIndex: operation.index,
26
+ flexDirection: operation.flexDirection,
27
+ children: operation.children,
28
+ wrapperStyles: operation.wrapperStyles
29
+ }
30
+ };
31
+ case 'dom.insert':
32
+ return {
33
+ kind: 'dom.insert',
34
+ summary: 'Insert new element into the page',
35
+ steps: [
36
+ `Locate parent "${operation.parentSelector}" in source.`,
37
+ `Insert the provided HTML at child index ${operation.index}.`,
38
+ 'Sanitize and adapt the HTML to the project’s component conventions before writing.'
39
+ ],
40
+ payload: {
41
+ parentSelector: operation.parentSelector,
42
+ insertIndex: operation.index,
43
+ html: operation.html,
44
+ selector: operation.selector
45
+ }
46
+ };
47
+ case 'dom.move':
48
+ return {
49
+ kind: 'dom.move',
50
+ summary: 'Move an existing element to a new parent',
51
+ steps: [
52
+ `Locate element "${operation.selector}" in source using selector and surrounding structure.`,
53
+ `Move it from "${operation.fromParentSelector}" (index ${operation.fromIndex}) to "${operation.toParentSelector}" at index ${operation.index}.`,
54
+ 'Preserve the element’s content and non-Ompo styling.'
55
+ ],
56
+ payload: {
57
+ selector: operation.selector,
58
+ fromParentSelector: operation.fromParentSelector,
59
+ fromIndex: operation.fromIndex,
60
+ toParentSelector: operation.toParentSelector,
61
+ insertIndex: operation.index
62
+ }
63
+ };
64
+ case 'dom.delete':
65
+ return {
66
+ kind: 'dom.delete',
67
+ summary: 'Remove an element from the page',
68
+ steps: [
69
+ `Locate and remove element "${operation.selector}" from source.`,
70
+ 'Do not remove surrounding structure unless required.'
71
+ ],
72
+ payload: {
73
+ selector: operation.selector
74
+ }
75
+ };
76
+ default:
77
+ return null;
78
+ }
79
+ }
80
+ export function buildApplyPlan(bundle) {
81
+ const files = new Map();
82
+ const domStructurePlans = [];
83
+ const domChanges = [];
84
+ const warnings = [];
85
+ if (bundle.scope.mode === 'subtree' && bundle.scope.rootSelector?.includes('nth-of-type')) {
86
+ warnings.push(`Subtree scope uses selector "${bundle.scope.rootSelector}". Confirm the matching component in source before applying.`);
87
+ }
88
+ for (const operation of bundle.operations) {
89
+ if (operation.kind === 'style') {
90
+ const existing = files.get(operation.selector) ?? {
91
+ selector: operation.selector,
92
+ suggestions: []
93
+ };
94
+ existing.suggestions.push(...styleSuggestions(operation));
95
+ files.set(operation.selector, existing);
96
+ if (operation.selector.includes('nth-of-type')) {
97
+ warnings.push(`Selector "${operation.selector}" may be unstable across builds. Prefer matching by component structure or stable attributes.`);
98
+ }
99
+ continue;
100
+ }
101
+ if (operation.kind === 'text') {
102
+ const existing = files.get(operation.selector) ?? {
103
+ selector: operation.selector,
104
+ suggestions: []
105
+ };
106
+ if (operation.textContent !== undefined) {
107
+ existing.suggestions.push({
108
+ property: 'textContent',
109
+ value: operation.textContent,
110
+ strategy: 'text-content'
111
+ });
112
+ }
113
+ if (operation.innerHTML !== undefined) {
114
+ existing.suggestions.push({
115
+ property: 'innerHTML',
116
+ value: operation.innerHTML,
117
+ strategy: 'component-markup',
118
+ notes: 'Sanitize inserted markup before writing to source files.'
119
+ });
120
+ }
121
+ files.set(operation.selector, existing);
122
+ continue;
123
+ }
124
+ const domPlan = buildDomStructurePlan(operation);
125
+ if (domPlan)
126
+ domStructurePlans.push(domPlan);
127
+ domChanges.push(operation);
128
+ if (operation.kind === 'dom.flexWrap') {
129
+ for (const child of operation.children) {
130
+ if (!child.id && !child.className && child.selector.includes('nth-of-type')) {
131
+ warnings.push(`Flex-wrap child "${child.selector}" lacks id/class. Use tag "${child.tagName}"${child.textSnippet ? ` and text "${child.textSnippet}"` : ''} to locate it in source.`);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ return {
137
+ editId: bundle.id,
138
+ scope: bundle.scope,
139
+ sourceUrl: bundle.source.url,
140
+ files: Array.from(files.values()),
141
+ domStructurePlans,
142
+ domChanges,
143
+ warnings: Array.from(new Set(warnings)),
144
+ instructions: [
145
+ 'Only apply properties and operations listed in this plan.',
146
+ 'Leave all untouched styling and markup unchanged.',
147
+ 'For dom.flexWrap: create a new wrapper, move matched children, then apply wrapperStyles.',
148
+ 'Use child anchors (tag, id, class, textSnippet) to find elements in source when selectors are unstable.',
149
+ 'Prefer the smallest possible diff for each file.'
150
+ ]
151
+ };
152
+ }
153
+ export function explainEdit(bundle) {
154
+ const styleCount = bundle.operations.filter((operation) => operation.kind === 'style').length;
155
+ const textCount = bundle.operations.filter((operation) => operation.kind === 'text').length;
156
+ const flexWrapCount = bundle.operations.filter((operation) => operation.kind === 'dom.flexWrap').length;
157
+ const domCount = bundle.operations.length - styleCount - textCount;
158
+ const scopeLabel = bundle.scope.mode === 'subtree' && bundle.scope.rootLabel
159
+ ? `${bundle.scope.rootLabel} and its children`
160
+ : 'all edits in the session';
161
+ const lines = [
162
+ `Edit ${bundle.id} contains ${bundle.meta.operationCount} operations for ${scopeLabel}.`,
163
+ `${styleCount} style change${styleCount === 1 ? '' : 's'}`,
164
+ `${textCount} text change${textCount === 1 ? '' : 's'}`,
165
+ `${domCount} DOM change${domCount === 1 ? '' : 's'}`
166
+ ];
167
+ if (flexWrapCount > 0) {
168
+ lines.push(`${flexWrapCount} flex-wrap operation${flexWrapCount === 1 ? '' : 's'}`);
169
+ }
170
+ lines.push(`Source preview: ${bundle.source.url}`);
171
+ return lines.join('\n');
172
+ }
@@ -0,0 +1,6 @@
1
+ import type { EditIndex, OmpoEditBundle } from './types.js';
2
+ export declare function findProjectRoot(startDir?: string): string | null;
3
+ export declare function listEdits(projectRoot: string): EditIndex['edits'];
4
+ export declare function readEditBundle(projectRoot: string, editId: string): OmpoEditBundle;
5
+ export declare function recordEditPull(projectRoot: string, editId: string): void;
6
+ export declare function recordEditApplied(projectRoot: string, editId: string): void;
@@ -0,0 +1,51 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { dirname, join, resolve } from 'path';
3
+ import { OMPo_EDITS_DIR } from './types.js';
4
+ export function findProjectRoot(startDir = process.cwd()) {
5
+ let current = resolve(startDir);
6
+ while (true) {
7
+ const editsDirectory = join(current, OMPo_EDITS_DIR);
8
+ if (existsSync(join(editsDirectory, 'index.json'))) {
9
+ return current;
10
+ }
11
+ const parent = dirname(current);
12
+ if (parent === current)
13
+ return null;
14
+ current = parent;
15
+ }
16
+ }
17
+ function readIndex(projectRoot) {
18
+ const indexPath = join(projectRoot, OMPo_EDITS_DIR, 'index.json');
19
+ return JSON.parse(readFileSync(indexPath, 'utf8'));
20
+ }
21
+ function writeIndex(projectRoot, index) {
22
+ const indexPath = join(projectRoot, OMPo_EDITS_DIR, 'index.json');
23
+ writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf8');
24
+ }
25
+ export function listEdits(projectRoot) {
26
+ return readIndex(projectRoot).edits;
27
+ }
28
+ export function readEditBundle(projectRoot, editId) {
29
+ const bundlePath = join(projectRoot, OMPo_EDITS_DIR, `${editId}.json`);
30
+ if (!existsSync(bundlePath)) {
31
+ throw new Error(`Edit not found: ${editId}`);
32
+ }
33
+ return JSON.parse(readFileSync(bundlePath, 'utf8'));
34
+ }
35
+ export function recordEditPull(projectRoot, editId) {
36
+ const index = readIndex(projectRoot);
37
+ const entry = index.edits.find((edit) => edit.id === editId);
38
+ if (!entry)
39
+ return;
40
+ entry.pulls += 1;
41
+ entry.lastPulledAt = new Date().toISOString();
42
+ writeIndex(projectRoot, index);
43
+ }
44
+ export function recordEditApplied(projectRoot, editId) {
45
+ const index = readIndex(projectRoot);
46
+ const entry = index.edits.find((edit) => edit.id === editId);
47
+ if (!entry)
48
+ return;
49
+ entry.appliedAt = new Date().toISOString();
50
+ writeIndex(projectRoot, index);
51
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { buildApplyPlan, explainEdit } from './apply-plan.js';
6
+ import { findProjectRoot, listEdits, readEditBundle, recordEditApplied, recordEditPull } from './edit-store.js';
7
+ const server = new McpServer({
8
+ name: 'ompo-mcp-server',
9
+ version: '0.1.0'
10
+ });
11
+ function requireProjectRoot() {
12
+ const projectRoot = findProjectRoot();
13
+ if (!projectRoot) {
14
+ throw new Error('No Ompo edits found. Run Send in Ompo first to create .ompo/edits in your project.');
15
+ }
16
+ return projectRoot;
17
+ }
18
+ server.tool('list_edits', 'List Ompo edit bundles saved in the current project', {}, async () => {
19
+ const projectRoot = requireProjectRoot();
20
+ const edits = listEdits(projectRoot);
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: JSON.stringify({ projectRoot, edits }, null, 2)
26
+ }
27
+ ]
28
+ };
29
+ });
30
+ server.tool('get_edit', 'Load a specific Ompo edit bundle by id', {
31
+ id: z.string().describe('Edit id, e.g. ed_8K42P')
32
+ }, async ({ id }) => {
33
+ const projectRoot = requireProjectRoot();
34
+ const bundle = readEditBundle(projectRoot, id);
35
+ recordEditPull(projectRoot, id);
36
+ return {
37
+ content: [
38
+ {
39
+ type: 'text',
40
+ text: JSON.stringify(bundle, null, 2)
41
+ }
42
+ ]
43
+ };
44
+ });
45
+ server.tool('explain_edit', 'Summarize what an Ompo edit changes', {
46
+ id: z.string().describe('Edit id, e.g. ed_8K42P')
47
+ }, async ({ id }) => {
48
+ const projectRoot = requireProjectRoot();
49
+ const bundle = readEditBundle(projectRoot, id);
50
+ recordEditPull(projectRoot, id);
51
+ return {
52
+ content: [
53
+ {
54
+ type: 'text',
55
+ text: explainEdit(bundle)
56
+ }
57
+ ]
58
+ };
59
+ });
60
+ server.tool('apply_edit', 'Build an apply plan for an Ompo edit. The agent should execute the plan against source files and only change listed properties.', {
61
+ id: z.string().describe('Edit id, e.g. ed_8K42P'),
62
+ markApplied: z
63
+ .boolean()
64
+ .optional()
65
+ .describe('Set true after the agent successfully applies the edit')
66
+ }, async ({ id, markApplied }) => {
67
+ const projectRoot = requireProjectRoot();
68
+ const bundle = readEditBundle(projectRoot, id);
69
+ recordEditPull(projectRoot, id);
70
+ const plan = buildApplyPlan(bundle);
71
+ if (markApplied) {
72
+ recordEditApplied(projectRoot, id);
73
+ }
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text',
78
+ text: JSON.stringify(plan, null, 2)
79
+ }
80
+ ]
81
+ };
82
+ });
83
+ async function main() {
84
+ const transport = new StdioServerTransport();
85
+ await server.connect(transport);
86
+ }
87
+ main().catch((error) => {
88
+ console.error(error);
89
+ process.exit(1);
90
+ });
@@ -0,0 +1,84 @@
1
+ export declare const OMPo_EDITS_DIR = ".ompo/edits";
2
+ export type EditScope = {
3
+ mode: 'session' | 'subtree';
4
+ rootSelector?: string;
5
+ rootLabel?: string;
6
+ };
7
+ export type StyleOperation = {
8
+ kind: 'style';
9
+ selector: string;
10
+ changed: Record<string, unknown>;
11
+ };
12
+ export type DomInsertOperation = {
13
+ kind: 'dom.insert';
14
+ parentSelector: string;
15
+ index: number;
16
+ html: string;
17
+ selector?: string;
18
+ };
19
+ export type DomMoveOperation = {
20
+ kind: 'dom.move';
21
+ selector: string;
22
+ fromParentSelector: string;
23
+ fromIndex: number;
24
+ toParentSelector: string;
25
+ index: number;
26
+ };
27
+ export type DomDeleteOperation = {
28
+ kind: 'dom.delete';
29
+ selector: string;
30
+ };
31
+ export type ElementAnchor = {
32
+ selector: string;
33
+ tagName: string;
34
+ id?: string;
35
+ className?: string;
36
+ textSnippet?: string;
37
+ };
38
+ export type DomFlexWrapOperation = {
39
+ kind: 'dom.flexWrap';
40
+ wrapperSelector: string;
41
+ parentSelector: string;
42
+ index: number;
43
+ flexDirection: 'row' | 'column';
44
+ children: ElementAnchor[];
45
+ wrapperStyles: Record<string, unknown>;
46
+ };
47
+ export type TextOperation = {
48
+ kind: 'text';
49
+ selector: string;
50
+ textContent?: string;
51
+ innerHTML?: string;
52
+ };
53
+ export type OmpoOperation = StyleOperation | DomInsertOperation | DomMoveOperation | DomDeleteOperation | DomFlexWrapOperation | TextOperation;
54
+ export type OmpoEditBundle = {
55
+ schemaVersion: number;
56
+ id: string;
57
+ createdAt: string;
58
+ ompoVersion: string;
59
+ source: {
60
+ url: string;
61
+ pathname: string;
62
+ };
63
+ scope: EditScope;
64
+ operations: OmpoOperation[];
65
+ meta: {
66
+ operationCount: number;
67
+ styleCount: number;
68
+ domCount: number;
69
+ };
70
+ };
71
+ export type EditIndexEntry = {
72
+ id: string;
73
+ createdAt: string;
74
+ scope: EditScope;
75
+ sourceUrl: string;
76
+ operationCount: number;
77
+ pulls: number;
78
+ lastPulledAt?: string;
79
+ appliedAt?: string;
80
+ };
81
+ export type EditIndex = {
82
+ version: 1;
83
+ edits: EditIndexEntry[];
84
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export const OMPo_EDITS_DIR = '.ompo/edits';
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@ompo-design/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for applying Ompo visual edits to a codebase",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "mcp",
9
+ "ompo",
10
+ "design",
11
+ "cursor",
12
+ "claude-code"
13
+ ],
14
+ "bin": {
15
+ "ompo-mcp": "./dist/index.js"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "start": "node dist/index.js",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.12.0",
28
+ "zod": "^3.24.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.15.3",
32
+ "typescript": "^5.8.3"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ }
37
+ }