@monoharada/wcf-mcp 0.1.2 → 0.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 +398 -8
- package/bin.mjs +19 -2
- package/core.mjs +1440 -104
- package/data/design-tokens.json +1708 -2
- package/data/guidelines-index.json +589 -3
- package/data/llms-full.txt +5291 -0
- package/examples/plugins/custom-validation-plugin.mjs +70 -0
- package/package.json +4 -2
- package/server.mjs +183 -5
- package/validator.mjs +601 -0
- package/wcf-mcp.config.example.json +24 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample plugin for wcf-mcp (Plugin Contract v1).
|
|
3
|
+
* Demonstrates:
|
|
4
|
+
* - Custom tool with handler
|
|
5
|
+
* - Custom tool using handler context (helpers.loadJsonData)
|
|
6
|
+
* - dataSources override for guidelines-index.json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function detectSkippedHeadingLevel(html = '') {
|
|
10
|
+
const headingMatches = [...String(html).matchAll(/<h([1-6])\b/gi)];
|
|
11
|
+
const levels = headingMatches.map((match) => Number(match[1]));
|
|
12
|
+
const diagnostics = [];
|
|
13
|
+
for (let index = 1; index < levels.length; index += 1) {
|
|
14
|
+
const prev = levels[index - 1];
|
|
15
|
+
const curr = levels[index];
|
|
16
|
+
if (curr > prev + 1) {
|
|
17
|
+
diagnostics.push({
|
|
18
|
+
code: 'skippedHeadingLevel',
|
|
19
|
+
message: `Heading level jumps from h${prev} to h${curr}.`,
|
|
20
|
+
index,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return diagnostics;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
name: 'custom-validation-plugin',
|
|
29
|
+
version: '0.2.0',
|
|
30
|
+
dataSources: [
|
|
31
|
+
{
|
|
32
|
+
fileName: 'guidelines-index.json',
|
|
33
|
+
path: './custom-guidelines.json',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
tools: [
|
|
37
|
+
{
|
|
38
|
+
name: 'validate_heading_structure',
|
|
39
|
+
description:
|
|
40
|
+
'Validate heading order in HTML. When: reviewing content hierarchy. Returns: diagnostics for skipped heading levels. After: fix heading structure before publication.',
|
|
41
|
+
inputSchema: {},
|
|
42
|
+
async handler(args = {}) {
|
|
43
|
+
const html = String(args?.html ?? '');
|
|
44
|
+
const diagnostics = detectSkippedHeadingLevel(html);
|
|
45
|
+
return {
|
|
46
|
+
diagnostics,
|
|
47
|
+
total: diagnostics.length,
|
|
48
|
+
ok: diagnostics.length === 0,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'list_custom_guidelines',
|
|
54
|
+
description:
|
|
55
|
+
'List custom organization guidelines. When: discovering org-specific guidelines. Returns: guideline document titles and topics.',
|
|
56
|
+
async handler(_args, { helpers }) {
|
|
57
|
+
const data = await helpers.loadJsonData('guidelines-index.json');
|
|
58
|
+
const documents = Array.isArray(data?.documents) ? data.documents : [];
|
|
59
|
+
return {
|
|
60
|
+
total: documents.length,
|
|
61
|
+
documents: documents.map((doc) => ({
|
|
62
|
+
id: doc.id,
|
|
63
|
+
title: doc.title,
|
|
64
|
+
topic: doc.topic,
|
|
65
|
+
})),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoharada/wcf-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for the web-components-factory design system. Provides component discovery, validation, and pattern-based UI composition without cloning the repository.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"core.mjs",
|
|
12
12
|
"server.mjs",
|
|
13
13
|
"validator.mjs",
|
|
14
|
-
"data/"
|
|
14
|
+
"data/",
|
|
15
|
+
"examples/",
|
|
16
|
+
"wcf-mcp.config.example.json"
|
|
15
17
|
],
|
|
16
18
|
"keywords": [
|
|
17
19
|
"mcp",
|
package/server.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
12
|
import { createMcpServer } from './core.mjs';
|
|
13
13
|
|
|
@@ -23,7 +23,9 @@ const REPO_FILE_MAP = {
|
|
|
23
23
|
'pattern-registry.json': 'registry/pattern-registry.json',
|
|
24
24
|
'design-tokens.json': 'design-tokens.json',
|
|
25
25
|
'guidelines-index.json': 'guidelines-index.json',
|
|
26
|
+
'llms-full.txt': 'llms-full.txt',
|
|
26
27
|
};
|
|
28
|
+
export const DEFAULT_WCF_MCP_CONFIG = 'wcf-mcp.config.json';
|
|
27
29
|
|
|
28
30
|
function resolveDataPath(fileName) {
|
|
29
31
|
const bundled = path.join(__dirname, 'data', fileName);
|
|
@@ -47,20 +49,196 @@ async function loadJsonData(fileName) {
|
|
|
47
49
|
throw new Error(`データファイルが見つかりません: ${fileName}`);
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
async function loadTextData(fileName) {
|
|
53
|
+
const { bundled, repo } = resolveDataPath(fileName);
|
|
54
|
+
for (const p of [bundled, repo]) {
|
|
55
|
+
if (!p) continue;
|
|
56
|
+
try {
|
|
57
|
+
return await fs.readFile(p, 'utf8');
|
|
58
|
+
} catch {
|
|
59
|
+
// Try next path
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`テキストデータファイルが見つかりません: ${fileName}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
async function loadValidator() {
|
|
51
66
|
return import('./validator.mjs');
|
|
52
67
|
}
|
|
53
68
|
|
|
69
|
+
function isPlainObject(value) {
|
|
70
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveFromBase(baseDir, maybeRelativePath) {
|
|
74
|
+
if (path.isAbsolute(maybeRelativePath)) return maybeRelativePath;
|
|
75
|
+
return path.resolve(baseDir, maybeRelativePath);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function pathExists(targetPath) {
|
|
79
|
+
try {
|
|
80
|
+
await fs.access(targetPath);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeDataSourcesInput(raw, baseDir, ownerLabel) {
|
|
88
|
+
if (!raw) return [];
|
|
89
|
+
const out = [];
|
|
90
|
+
if (Array.isArray(raw)) {
|
|
91
|
+
for (const item of raw) {
|
|
92
|
+
if (!isPlainObject(item)) {
|
|
93
|
+
throw new Error(`Invalid ${ownerLabel}.dataSources entry: expected object`);
|
|
94
|
+
}
|
|
95
|
+
const fileName = String(item.fileName ?? '').trim();
|
|
96
|
+
const sourcePath = String(item.path ?? '').trim();
|
|
97
|
+
if (!fileName || !sourcePath) {
|
|
98
|
+
throw new Error(`Invalid ${ownerLabel}.dataSources entry: fileName and path are required`);
|
|
99
|
+
}
|
|
100
|
+
out.push({ fileName, path: resolveFromBase(baseDir, sourcePath) });
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
if (isPlainObject(raw)) {
|
|
105
|
+
for (const [fileName, sourcePath] of Object.entries(raw)) {
|
|
106
|
+
const source = String(sourcePath ?? '').trim();
|
|
107
|
+
if (!fileName || !source) continue;
|
|
108
|
+
out.push({ fileName, path: resolveFromBase(baseDir, source) });
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Invalid ${ownerLabel}.dataSources: expected array or object map`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function loadModulePlugin(modulePath, baseDir) {
|
|
116
|
+
const absPath = resolveFromBase(baseDir, modulePath);
|
|
117
|
+
const loaded = await import(pathToFileURL(absPath).href);
|
|
118
|
+
const candidate = loaded?.default ?? loaded?.plugin ?? loaded;
|
|
119
|
+
const plugin = typeof candidate === 'function' ? await candidate() : candidate;
|
|
120
|
+
if (!isPlainObject(plugin)) {
|
|
121
|
+
throw new Error(`Invalid plugin module: ${modulePath} (expected plugin object export)`);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
plugin,
|
|
125
|
+
moduleDir: path.dirname(absPath),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeStaticTools(staticTools, ownerLabel) {
|
|
130
|
+
if (!Array.isArray(staticTools)) return [];
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const entry of staticTools) {
|
|
133
|
+
if (!isPlainObject(entry)) {
|
|
134
|
+
throw new Error(`Invalid ${ownerLabel}.staticTools entry: expected object`);
|
|
135
|
+
}
|
|
136
|
+
const name = String(entry.name ?? '').trim();
|
|
137
|
+
if (!name) throw new Error(`Invalid ${ownerLabel}.staticTools entry: name is required`);
|
|
138
|
+
const description = String(entry.description ?? '').trim() || `Static plugin tool (${ownerLabel})`;
|
|
139
|
+
out.push({
|
|
140
|
+
name,
|
|
141
|
+
description,
|
|
142
|
+
inputSchema: {},
|
|
143
|
+
staticPayload: Object.prototype.hasOwnProperty.call(entry, 'payload')
|
|
144
|
+
? entry.payload
|
|
145
|
+
: { message: `Static tool response from ${name}` },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function loadWcfMcpRuntimeConfig({ cwd = process.cwd(), configPath } = {}) {
|
|
152
|
+
const resolvedConfigPath = configPath
|
|
153
|
+
? resolveFromBase(cwd, configPath)
|
|
154
|
+
: path.join(cwd, DEFAULT_WCF_MCP_CONFIG);
|
|
155
|
+
const exists = await pathExists(resolvedConfigPath);
|
|
156
|
+
if (!exists) {
|
|
157
|
+
if (configPath) {
|
|
158
|
+
throw new Error(`Config file not found: ${resolvedConfigPath}`);
|
|
159
|
+
}
|
|
160
|
+
return { configPath: resolvedConfigPath, plugins: [] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const text = await fs.readFile(resolvedConfigPath, 'utf8');
|
|
164
|
+
const rawConfig = JSON.parse(text);
|
|
165
|
+
if (!isPlainObject(rawConfig)) {
|
|
166
|
+
throw new Error(`Invalid config: ${resolvedConfigPath} must contain a JSON object`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const configDir = path.dirname(resolvedConfigPath);
|
|
170
|
+
const plugins = [];
|
|
171
|
+
const rootDataSources = normalizeDataSourcesInput(rawConfig.dataSources, configDir, 'config');
|
|
172
|
+
if (rootDataSources.length > 0) {
|
|
173
|
+
plugins.push({
|
|
174
|
+
name: 'config-data-sources',
|
|
175
|
+
version: '0.0.1',
|
|
176
|
+
dataSources: rootDataSources,
|
|
177
|
+
tools: [],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const rawPlugins = Array.isArray(rawConfig.plugins) ? rawConfig.plugins : [];
|
|
182
|
+
for (const item of rawPlugins) {
|
|
183
|
+
if (!isPlainObject(item)) {
|
|
184
|
+
throw new Error('Invalid config.plugins entry: expected object');
|
|
185
|
+
}
|
|
186
|
+
if (typeof item.module === 'string' && item.module.trim() !== '') {
|
|
187
|
+
const { plugin: loadedPlugin, moduleDir } = await loadModulePlugin(item.module, configDir);
|
|
188
|
+
const moduleDataSources = normalizeDataSourcesInput(
|
|
189
|
+
loadedPlugin.dataSources,
|
|
190
|
+
moduleDir,
|
|
191
|
+
`plugin(${item.module})`,
|
|
192
|
+
);
|
|
193
|
+
plugins.push({
|
|
194
|
+
...loadedPlugin,
|
|
195
|
+
dataSources: moduleDataSources,
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const name = String(item.name ?? '').trim();
|
|
201
|
+
const version = String(item.version ?? '').trim();
|
|
202
|
+
if (!name || !version) {
|
|
203
|
+
throw new Error('Invalid config.plugins static entry: name and version are required');
|
|
204
|
+
}
|
|
205
|
+
plugins.push({
|
|
206
|
+
name,
|
|
207
|
+
version,
|
|
208
|
+
dataSources: normalizeDataSourcesInput(item.dataSources, configDir, `plugin(${name})`),
|
|
209
|
+
tools: normalizeStaticTools(item.staticTools, `plugin(${name})`),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
configPath: resolvedConfigPath,
|
|
215
|
+
plugins,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function loadJsonDataFromPath(sourcePath) {
|
|
220
|
+
const text = await fs.readFile(sourcePath, 'utf8');
|
|
221
|
+
return JSON.parse(text);
|
|
222
|
+
}
|
|
223
|
+
|
|
54
224
|
// ---------------------------------------------------------------------------
|
|
55
225
|
// Public API
|
|
56
226
|
// ---------------------------------------------------------------------------
|
|
57
227
|
|
|
58
|
-
export async function createServer() {
|
|
59
|
-
|
|
228
|
+
export async function createServer(options = {}) {
|
|
229
|
+
const runtimeConfig = await loadWcfMcpRuntimeConfig({
|
|
230
|
+
cwd: options.cwd ?? process.cwd(),
|
|
231
|
+
configPath: options.configPath,
|
|
232
|
+
});
|
|
233
|
+
return createMcpServer(loadJsonData, loadValidator, {
|
|
234
|
+
plugins: runtimeConfig.plugins,
|
|
235
|
+
loadJsonDataFromPath,
|
|
236
|
+
loadTextData,
|
|
237
|
+
});
|
|
60
238
|
}
|
|
61
239
|
|
|
62
|
-
export async function startServer() {
|
|
63
|
-
const { server } = await createServer();
|
|
240
|
+
export async function startServer(options = {}) {
|
|
241
|
+
const { server } = await createServer(options);
|
|
64
242
|
const transport = new StdioServerTransport();
|
|
65
243
|
await server.connect(transport);
|
|
66
244
|
}
|