Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [1.6.0-beta.0](https://github.com/wireweave/mcp-server/compare/v1.5.1...v1.6.0-beta.0) (2026-03-04)

### Features

* **tools:** add local HTML file rendering tool ([40d46e1](https://github.com/wireweave/mcp-server/commit/40d46e10d619bea4834c4dcb6dacf9dd0b9cd949))

## [1.5.1](https://github.com/wireweave/mcp-server/compare/v1.5.1-beta.0...v1.5.1) (2026-02-17)

## [1.5.1-beta.0](https://github.com/wireweave/mcp-server/compare/v1.5.0...v1.5.1-beta.0) (2026-02-17)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wireweave/mcp-server",
"version": "1.5.1",
"version": "1.6.0-beta.0",
"description": "MCP server for Wireweave DSL - Thin client for API Server",
"type": "module",
"main": "dist/index.js",
Expand Down
15 changes: 12 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { tools, toolEndpoints } from './tools.js';
import { prompts, promptTemplates } from './prompts.js';
import { resources, resourceToTool } from './resources.js';
import { callApi, type ApiConfig } from './api.js';
import { localTools, LOCAL_TOOL_NAMES, handleLocalTool } from './local-tools.js';

// API Server URL
const API_URL = process.env.WIREWEAVE_API_URL || 'https://api.wireweave.org';
Expand Down Expand Up @@ -53,22 +54,30 @@ const server = new Server(

// Handle tools/list request
server.setRequestHandler(ListToolsRequestSchema, async (): Promise<ListToolsResult> => {
return { tools };
// Merge auto-generated tools with local-only tools
return { tools: [...tools, ...localTools] };
});

// Handle tools/call request
server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
const { name, arguments: args } = request.params;

// Check for local-only tools first
if (LOCAL_TOOL_NAMES.has(name)) {
return handleLocalTool(name, args as Record<string, unknown>, apiConfig);
}

// API proxy for auto-generated tools
const endpoint = toolEndpoints[name];
if (!endpoint) {
const allTools = [...tools, ...localTools];
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Unknown tool: ${name}`,
availableTools: tools.map((t) => t.name),
availableTools: allTools.map((t) => t.name),
}, null, 2),
},
],
Expand Down Expand Up @@ -203,7 +212,7 @@ async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);

log(`MCP server started with ${tools.length} tools, ${prompts.length} prompts, ${resources.length} resources`);
log(`MCP server started with ${tools.length + localTools.length} tools (${localTools.length} local), ${prompts.length} prompts, ${resources.length} resources`);

if (!API_KEY) {
log('API key not configured. Get one at https://wireweave.org', 'warn');
Expand Down
199 changes: 199 additions & 0 deletions src/local-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* Local-only MCP Tools
*
* These tools are handled locally without full API proxy.
* They are NOT auto-generated and should be maintained manually.
*/

import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { callApi, type ApiConfig } from './api.js';
import { toolEndpoints } from './tools.js';

/**
* Local tool definitions
*/
export const localTools: Tool[] = [
{
name: 'wireweave_render_html_file',
description:
'Render Wireweave DSL to HTML and save to a local file. Returns the file path. Use this when you need a persistent HTML file for preview or browser viewing. Credits are charged via internal API call.',
inputSchema: {
type: 'object',
properties: {
source: {
type: 'string',
description: 'The Wireweave DSL source code to render',
},
theme: {
type: 'string',
enum: ['light', 'dark'],
description: 'Color theme for rendering',
default: 'light',
},
outputDir: {
type: 'string',
description:
'Output directory for the HTML file. Defaults to system temp directory.',
},
filename: {
type: 'string',
description:
'Custom filename without extension. Defaults to wireframe-{timestamp}.',
},
},
required: ['source'],
},
},
];

/**
* Set of local tool names for quick lookup
*/
export const LOCAL_TOOL_NAMES = new Set(localTools.map((t) => t.name));

/**
* Handle local tool calls
*/
export async function handleLocalTool(
name: string,
args: Record<string, unknown>,
apiConfig: ApiConfig
): Promise<CallToolResult> {
switch (name) {
case 'wireweave_render_html_file':
return handleRenderHtmlFile(args, apiConfig);
default:
return {
content: [
{
type: 'text',
text: JSON.stringify({ error: `Unknown local tool: ${name}` }, null, 2),
},
],
isError: true,
};
}
}

/**
* Handle wireweave_render_html_file
*
* 1. Call API to render HTML (credits are charged)
* 2. Save to local file
* 3. Return file path
*/
async function handleRenderHtmlFile(
args: Record<string, unknown>,
apiConfig: ApiConfig
): Promise<CallToolResult> {
const { source, theme = 'light', outputDir, filename } = args;

// Validate source
if (!source || typeof source !== 'string') {
return {
content: [
{
type: 'text',
text: JSON.stringify({ error: 'source is required' }, null, 2),
},
],
isError: true,
};
}

try {
// 1. Call API to render HTML (this charges credits)
const endpoint = toolEndpoints['wireweave_render_html'];
if (!endpoint) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{ error: 'wireweave_render_html endpoint not found' },
null,
2
),
},
],
isError: true,
};
}

const result = (await callApi(apiConfig, endpoint, {
source,
theme,
fullDocument: true, // Always request full document for file output
})) as { success?: boolean; html?: string; css?: string; error?: string };

if (!result.success || !result.html) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{ error: result.error || 'No HTML content returned from API' },
null,
2
),
},
],
isError: true,
};
}

// 2. Prepare file path
const dir = typeof outputDir === 'string' ? outputDir : os.tmpdir();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fname = typeof filename === 'string' ? filename : `wireframe-${timestamp}`;
const filePath = path.join(dir, `${fname}.html`);

// 3. Build HTML content with embedded CSS
let htmlContent = result.html;
if (result.css && !htmlContent.includes('<style>')) {
// Inject CSS into head
htmlContent = htmlContent.replace('</head>', `<style>${result.css}</style></head>`);
}

// 4. Write to file
fs.writeFileSync(filePath, htmlContent, 'utf-8');

// 5. Return result
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
filePath,
message: `HTML file saved to: ${filePath}`,
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: error instanceof Error ? error.message : 'Failed to render HTML file',
},
null,
2
),
},
],
isError: true,
};
}
}
2 changes: 1 addition & 1 deletion src/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* ⚠️ AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
*
* Generated: 2026-02-12T08:54:48.791Z
* Generated: 2026-03-04T11:10:35.311Z
* Public prompts: 3
*/

Expand Down
2 changes: 1 addition & 1 deletion src/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* ⚠️ AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
*
* Generated: 2026-02-12T08:54:48.961Z
* Generated: 2026-03-04T11:10:35.479Z
* Public resources: 5
*/

Expand Down
35 changes: 33 additions & 2 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* To request changes, please open an issue at:
* https://github.com/wireweave/mcp-server/issues
*
* Generated: 2026-02-12T08:54:48.586Z
* Public tools: 29
* Generated: 2026-03-04T11:10:35.125Z
* Public tools: 30
*/

import type { Tool } from '@modelcontextprotocol/sdk/types.js';
Expand Down Expand Up @@ -138,6 +138,36 @@ export const tools: Tool[] = [
]
},
},
{
name: 'wireweave_render_html_code',
description: 'Render Wireweave DSL to HTML code. Returns the HTML content directly. This is an alias for wireweave_render_html with explicit naming.',
inputSchema: {
"type": "object",
"properties": {
"source": {
"type": "string",
"description": "The Wireweave DSL source code to render"
},
"theme": {
"type": "string",
"enum": [
"light",
"dark"
],
"description": "Color theme for rendering",
"default": "light"
},
"fullDocument": {
"type": "boolean",
"description": "Return a complete HTML document instead of fragment",
"default": false
}
},
"required": [
"source"
]
},
},
{
name: 'wireweave_validate_ux',
description: 'Validate Wireweave DSL for UX best practices. Returns issues with severity levels and actionable recommendations.',
Expand Down Expand Up @@ -694,6 +724,7 @@ export const toolEndpoints: Record<string, ToolEndpoint> = {
wireweave_patterns: { method: 'GET', path: '/tools/patterns' },
wireweave_examples: { method: 'GET', path: '/tools/examples' },
wireweave_render_html: { method: 'POST', path: '/tools/render/html' },
wireweave_render_html_code: { method: 'POST', path: '/tools/render/html' },
wireweave_validate_ux: { method: 'POST', path: '/tools/validate/ux' },
wireweave_ux_rules: { method: 'GET', path: '/tools/ux-rules' },
wireweave_diff: { method: 'POST', path: '/tools/diff' },
Expand Down