@runsnative/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 +96 -0
- package/dist/content.js +324 -0
- package/dist/http-server.js +213 -0
- package/dist/index.js +72 -0
- package/dist/inferrer/image-extractor.js +76 -0
- package/dist/inferrer/page-fetcher.js +46 -0
- package/dist/inferrer/pipeline.js +27 -0
- package/dist/inferrer/style-extractor.js +75 -0
- package/dist/inferrer/theme-inferrer.js +134 -0
- package/dist/local-provider.js +38 -0
- package/dist/provider.js +27 -0
- package/dist/remote-provider.js +339 -0
- package/dist/test/validate.js +315 -0
- package/dist/tools/get-component.js +47 -0
- package/dist/tools/get-composition-pattern.js +43 -0
- package/dist/tools/get-foundation.js +38 -0
- package/dist/tools/get-step.js +44 -0
- package/dist/tools/infer-brand-theme.js +29 -0
- package/dist/tools/infer-theme.js +95 -0
- package/dist/tools/list-components.js +35 -0
- package/dist/tools/list-composition-patterns.js +29 -0
- package/dist/tools/list-exercises.js +23 -0
- package/dist/tools/search-docs.js +43 -0
- package/dist/tools/start-exercise.js +36 -0
- package/dist/tools/surface-preview.js +36 -0
- package/package.json +36 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const GET_COMPONENT_TOOL = {
|
|
3
|
+
name: 'get_component',
|
|
4
|
+
description: 'Returns RunsNative component documentation for a specific tab. ' +
|
|
5
|
+
'Tabs: usage (what it is and how to use it), style (tokens, CSS parts, theming), ' +
|
|
6
|
+
'code (API: properties, events, slots, framework integration), ' +
|
|
7
|
+
'accessibility (ARIA, keyboard, screen reader, WCAG). ' +
|
|
8
|
+
'Call usage first for orientation, code for generating correct markup.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
name: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Component slug — the element tag without the "run-" prefix. Examples: "button", "tabs", "text-field".',
|
|
15
|
+
},
|
|
16
|
+
tab: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
enum: ['usage', 'style', 'code', 'accessibility'],
|
|
19
|
+
description: 'Which documentation tab to retrieve.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ['name', 'tab'],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export async function handleGetComponent(provider, args) {
|
|
26
|
+
const name = args.name;
|
|
27
|
+
const tab = args.tab;
|
|
28
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
29
|
+
throw new McpError(ErrorCode.InvalidParams, 'name must be a non-empty string');
|
|
30
|
+
}
|
|
31
|
+
if (typeof tab !== 'string') {
|
|
32
|
+
throw new McpError(ErrorCode.InvalidParams, 'tab must be a string');
|
|
33
|
+
}
|
|
34
|
+
let content;
|
|
35
|
+
try {
|
|
36
|
+
content = await provider.getComponent(name.trim(), tab.trim());
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (err instanceof RangeError) {
|
|
40
|
+
throw new McpError(ErrorCode.InvalidParams, err.message);
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: 'text', text: content }],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const GET_COMPOSITION_PATTERN_TOOL = {
|
|
3
|
+
name: 'get_composition_pattern',
|
|
4
|
+
description: 'Returns the full composition pattern for the named recipe. ' +
|
|
5
|
+
'Includes a live preview spec, code examples in HTML, React, Vue, Angular, and Svelte, ' +
|
|
6
|
+
'components used, accessibility notes, and customization guidance. ' +
|
|
7
|
+
'Use list_composition_patterns to discover available pattern names.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
name: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Pattern slug, e.g. "sign-in-form", "dashboard-shell", "multi-step-form". ' +
|
|
14
|
+
'Use list_composition_patterns to see all available names.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
required: ['name'],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export async function handleGetCompositionPattern(provider, args) {
|
|
21
|
+
const name = args['name'];
|
|
22
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
23
|
+
throw new McpError(ErrorCode.InvalidParams, 'name is required');
|
|
24
|
+
}
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = await provider.getRecipe(name.trim());
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err instanceof RangeError) {
|
|
31
|
+
throw new McpError(ErrorCode.InvalidParams, err.message);
|
|
32
|
+
}
|
|
33
|
+
if (err instanceof McpError)
|
|
34
|
+
throw err;
|
|
35
|
+
if (err instanceof Error) {
|
|
36
|
+
throw new McpError(ErrorCode.InternalError, err.message);
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: content }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const GET_FOUNDATION_TOOL = {
|
|
3
|
+
name: 'get_foundation',
|
|
4
|
+
description: 'Returns RunsNative design foundation documentation. ' +
|
|
5
|
+
'Foundations cover the token system: color, typography, spacing, grid, motion, elevation, icons. ' +
|
|
6
|
+
'Use this to answer questions about design tokens, CSS custom properties, and system-level decisions.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
name: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Foundation slug. One of: color, typography, spacing, grid, motion, elevation, icons.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
required: ['name'],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export async function handleGetFoundation(provider, args) {
|
|
19
|
+
const name = args.name;
|
|
20
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
21
|
+
const foundations = await provider.listFoundations();
|
|
22
|
+
const names = foundations.map((f) => f.foundation).join(', ');
|
|
23
|
+
throw new McpError(ErrorCode.InvalidParams, `name must be a non-empty string. Available foundations: ${names}.`);
|
|
24
|
+
}
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = await provider.getFoundation(name.trim());
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err instanceof RangeError) {
|
|
31
|
+
const foundations = await provider.listFoundations();
|
|
32
|
+
const names = foundations.map((f) => f.foundation).join(', ');
|
|
33
|
+
throw new McpError(ErrorCode.InvalidParams, `${err.message} Available foundations: ${names}.`);
|
|
34
|
+
}
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
return { content: [{ type: 'text', text: content }] };
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const GET_STEP_TOOL = {
|
|
3
|
+
name: 'get_step',
|
|
4
|
+
description: 'Returns one step from a RunsNative exercise. ' +
|
|
5
|
+
'The goal field is a one-sentence summary; content is the full step body (instructions, what to expect, troubleshooting). ' +
|
|
6
|
+
'Call start_exercise(name) first to get the step list and total step count.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
exercise: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Exercise slug. Examples: "vibe-with-runsnative", "migrate-to-runsnative".',
|
|
13
|
+
},
|
|
14
|
+
step: {
|
|
15
|
+
type: 'number',
|
|
16
|
+
description: 'Step number (1-indexed).',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ['exercise', 'step'],
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export async function handleGetStep(provider, args) {
|
|
23
|
+
const exercise = args.exercise;
|
|
24
|
+
const step = args.step;
|
|
25
|
+
if (typeof exercise !== 'string' || !exercise.trim()) {
|
|
26
|
+
throw new McpError(ErrorCode.InvalidParams, 'exercise must be a non-empty string');
|
|
27
|
+
}
|
|
28
|
+
if (typeof step !== 'number' || !Number.isInteger(step) || step < 1) {
|
|
29
|
+
throw new McpError(ErrorCode.InvalidParams, 'step must be a positive integer');
|
|
30
|
+
}
|
|
31
|
+
let result;
|
|
32
|
+
try {
|
|
33
|
+
result = await provider.getExerciseStep(exercise.trim(), step);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err instanceof RangeError) {
|
|
37
|
+
throw new McpError(ErrorCode.InvalidParams, err.message);
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { runInferencePipeline } from '../inferrer/pipeline.js';
|
|
3
|
+
export const INFER_BRAND_THEME_TOOL = {
|
|
4
|
+
name: 'infer_brand_theme',
|
|
5
|
+
description: 'Fetches a URL via headless browser, extracts CSS and image signals, and returns an ' +
|
|
6
|
+
'InferenceResult with a resolved InferredTheme and per-field confidence metadata. ' +
|
|
7
|
+
'Requires playwright to be installed. Use infer_theme for HTML-only analysis.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
url: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'The page URL to fetch and analyse (http or https).',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
required: ['url'],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
export async function handleInferBrandTheme(args) {
|
|
20
|
+
const url = args.url;
|
|
21
|
+
if (typeof url !== 'string' || !url.trim()) {
|
|
22
|
+
throw new McpError(ErrorCode.InvalidParams, 'url must be a non-empty string');
|
|
23
|
+
}
|
|
24
|
+
const pipelineResult = await runInferencePipeline(url);
|
|
25
|
+
if (!pipelineResult.ok) {
|
|
26
|
+
throw new McpError(ErrorCode.InternalError, `Inference pipeline failed: ${pipelineResult.error.message}`);
|
|
27
|
+
}
|
|
28
|
+
return { content: [{ type: 'text', text: JSON.stringify(pipelineResult.result, null, 2) }] };
|
|
29
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const INFER_THEME_TOOL = {
|
|
3
|
+
name: 'infer_theme',
|
|
4
|
+
description: 'Infers the RunsNative skin/theme in use from HTML markup or a page URL. ' +
|
|
5
|
+
'Analyses CSS custom property overrides and class-based skin selectors to identify ' +
|
|
6
|
+
'which skin is active. Returns skin name, confidence, detected token overrides, and reasoning. ' +
|
|
7
|
+
'Returns low-confidence stub when skin token data is unavailable.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
html: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'HTML snippet or full document to analyse.',
|
|
14
|
+
},
|
|
15
|
+
url: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Optional page URL for additional context.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
required: ['html'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
// Skin selector patterns — maps skin name to CSS class / data-attribute markers.
|
|
24
|
+
const SKIN_PATTERNS = [
|
|
25
|
+
{
|
|
26
|
+
skin: 'default',
|
|
27
|
+
patterns: [/data-skin="default"/, /class="[^"]*run-skin-default[^"]*"/],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
skin: 'dark',
|
|
31
|
+
patterns: [/data-theme="dark"/, /data-skin="dark"/, /class="[^"]*run-skin-dark[^"]*"/, /class="[^"]*dark-mode[^"]*"/],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
skin: 'high-contrast',
|
|
35
|
+
patterns: [/data-skin="high-contrast"/, /class="[^"]*run-skin-high-contrast[^"]*"/, /data-theme="high-contrast"/],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
// CSS custom property token pattern — matches --run-* overrides in style attributes or <style> blocks.
|
|
39
|
+
const TOKEN_OVERRIDE_RE = /--run-([\w-]+)\s*:\s*([^;}"]+)/g;
|
|
40
|
+
function analyseHtml(html) {
|
|
41
|
+
// Extract token overrides from inline styles and <style> blocks.
|
|
42
|
+
const token_overrides = {};
|
|
43
|
+
let match;
|
|
44
|
+
TOKEN_OVERRIDE_RE.lastIndex = 0;
|
|
45
|
+
while ((match = TOKEN_OVERRIDE_RE.exec(html)) !== null) {
|
|
46
|
+
token_overrides[`--run-${match[1]}`] = match[2].trim();
|
|
47
|
+
}
|
|
48
|
+
// Match skin by selector patterns.
|
|
49
|
+
for (const { skin, patterns } of SKIN_PATTERNS) {
|
|
50
|
+
const matched = patterns.filter((p) => p.test(html));
|
|
51
|
+
if (matched.length > 0) {
|
|
52
|
+
const overrideCount = Object.keys(token_overrides).length;
|
|
53
|
+
const confidence = overrideCount > 0 ? 'high' : 'medium';
|
|
54
|
+
return {
|
|
55
|
+
skin,
|
|
56
|
+
confidence,
|
|
57
|
+
token_overrides,
|
|
58
|
+
reasoning: `Matched skin selector pattern for "${skin}". ` +
|
|
59
|
+
(overrideCount > 0
|
|
60
|
+
? `Found ${overrideCount} CSS custom property override(s).`
|
|
61
|
+
: 'No CSS custom property overrides detected.'),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Check for any --run-color-* overrides that might hint at theming without a named skin.
|
|
66
|
+
const overrideCount = Object.keys(token_overrides).length;
|
|
67
|
+
if (overrideCount > 0) {
|
|
68
|
+
return {
|
|
69
|
+
skin: null,
|
|
70
|
+
confidence: 'medium',
|
|
71
|
+
token_overrides,
|
|
72
|
+
reasoning: `No named skin selector found, but detected ${overrideCount} CSS custom property override(s) using RunsNative tokens.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Fallback stub — skin token data not yet loaded or markup has no skin signals.
|
|
76
|
+
return {
|
|
77
|
+
skin: null,
|
|
78
|
+
confidence: 'low',
|
|
79
|
+
token_overrides: {},
|
|
80
|
+
reasoning: 'No skin selector or RunsNative token overrides detected in the provided markup. ' +
|
|
81
|
+
'Skin token data may not yet be available for this build, or the page uses the default skin implicitly.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export async function handleInferTheme(args) {
|
|
85
|
+
const html = args.html;
|
|
86
|
+
if (typeof html !== 'string') {
|
|
87
|
+
throw new McpError(ErrorCode.InvalidParams, 'html must be a string');
|
|
88
|
+
}
|
|
89
|
+
if (!html.trim()) {
|
|
90
|
+
throw new McpError(ErrorCode.InvalidParams, 'html must not be empty');
|
|
91
|
+
}
|
|
92
|
+
const result = analyseHtml(html);
|
|
93
|
+
const text = JSON.stringify(result, null, 2);
|
|
94
|
+
return { content: [{ type: 'text', text }] };
|
|
95
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const LIST_COMPONENTS_TOOL = {
|
|
3
|
+
name: 'list_components',
|
|
4
|
+
description: 'Lists RunsNative components available in the knowledge base. ' +
|
|
5
|
+
'Returns name, title, element tag, surface area, status, and purpose for each component. ' +
|
|
6
|
+
'By default returns only ready components. Pass include_drafts: true to include draft stubs.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
include_drafts: {
|
|
11
|
+
type: 'boolean',
|
|
12
|
+
description: 'When true, includes draft/stub components in addition to ready ones. Default: false.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
required: [],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export async function handleListComponents(provider, args) {
|
|
19
|
+
const includeDrafts = args.include_drafts === true;
|
|
20
|
+
let components;
|
|
21
|
+
try {
|
|
22
|
+
components = await provider.listComponents(includeDrafts);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err instanceof Error) {
|
|
26
|
+
throw new McpError(ErrorCode.InternalError, err.message);
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
const payload = { components };
|
|
31
|
+
if (provider.bootstrapUrl)
|
|
32
|
+
payload['bootstrap_url'] = provider.bootstrapUrl;
|
|
33
|
+
const text = JSON.stringify(payload, null, 2);
|
|
34
|
+
return { content: [{ type: 'text', text }] };
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const LIST_COMPOSITION_PATTERNS_TOOL = {
|
|
3
|
+
name: 'list_composition_patterns',
|
|
4
|
+
description: 'Lists available RunsNative composition patterns (recipes). ' +
|
|
5
|
+
'Each pattern shows how to compose a complete UI surface — sign-in form, dashboard shell, ' +
|
|
6
|
+
'multi-step form, chat room, etc. — using RunsNative components, with live preview specs, ' +
|
|
7
|
+
'framework code examples, accessibility notes, and customization guidance. ' +
|
|
8
|
+
'Call get_composition_pattern(name) to retrieve the full pattern.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {},
|
|
12
|
+
required: [],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export async function handleListCompositionPatterns(provider, _args) {
|
|
16
|
+
let patterns;
|
|
17
|
+
try {
|
|
18
|
+
patterns = await provider.listRecipes();
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (err instanceof Error) {
|
|
22
|
+
throw new McpError(ErrorCode.InternalError, err.message);
|
|
23
|
+
}
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: JSON.stringify({ patterns }, null, 2) }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const LIST_EXERCISES_TOOL = {
|
|
2
|
+
name: 'list_exercises',
|
|
3
|
+
description: 'Returns the available RunsNative guided exercises. ' +
|
|
4
|
+
'Each exercise is activity-shaped (vibe, migrate, compose, explore) with a difficulty level and estimated time. ' +
|
|
5
|
+
'Call start_exercise(name) to begin an exercise and get its step list.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
include_drafts: {
|
|
10
|
+
type: 'boolean',
|
|
11
|
+
description: 'Include exercises with status: draft. Defaults to false (ready exercises only).',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
required: [],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export async function handleListExercises(provider, args) {
|
|
18
|
+
const includeDrafts = args.include_drafts === true;
|
|
19
|
+
const exercises = await provider.listExercises(includeDrafts);
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: 'text', text: JSON.stringify({ exercises }, null, 2) }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const SEARCH_DOCS_TOOL = {
|
|
3
|
+
name: 'search_docs',
|
|
4
|
+
description: 'Lexical search across all RunsNative component documentation, foundations, and exercises. ' +
|
|
5
|
+
'Useful when you do not know the exact component name — for example, searching "focus ring token" ' +
|
|
6
|
+
'or "which component handles multi-select". Returns scored results with excerpts.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
query: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Search terms, e.g. "focus ring token" or "accessible checkbox".',
|
|
13
|
+
},
|
|
14
|
+
limit: {
|
|
15
|
+
type: 'number',
|
|
16
|
+
description: 'Maximum number of results to return. Default: 5.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ['query'],
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export async function handleSearchDocs(provider, args) {
|
|
23
|
+
const query = args.query;
|
|
24
|
+
const limit = args.limit;
|
|
25
|
+
if (typeof query !== 'string') {
|
|
26
|
+
throw new McpError(ErrorCode.InvalidParams, 'query must be a string');
|
|
27
|
+
}
|
|
28
|
+
if (limit !== undefined && (typeof limit !== 'number' || limit < 1)) {
|
|
29
|
+
throw new McpError(ErrorCode.InvalidParams, 'limit must be a positive number');
|
|
30
|
+
}
|
|
31
|
+
let results;
|
|
32
|
+
try {
|
|
33
|
+
results = await provider.searchDocs(query.trim(), typeof limit === 'number' ? Math.floor(limit) : 5);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err instanceof Error) {
|
|
37
|
+
throw new McpError(ErrorCode.InternalError, err.message);
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
const text = JSON.stringify({ query, results }, null, 2);
|
|
42
|
+
return { content: [{ type: 'text', text }] };
|
|
43
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const START_EXERCISE_TOOL = {
|
|
3
|
+
name: 'start_exercise',
|
|
4
|
+
description: 'Returns the overview and step list for a RunsNative exercise. ' +
|
|
5
|
+
'Does not return step content — call get_step(exercise, n) for each step individually. ' +
|
|
6
|
+
'Use list_exercises() first to see available exercise names.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
name: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Exercise slug. Examples: "vibe-with-runsnative", "migrate-to-runsnative", "compose-with-compounds".',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
required: ['name'],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export async function handleStartExercise(provider, args) {
|
|
19
|
+
const name = args.name;
|
|
20
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
21
|
+
throw new McpError(ErrorCode.InvalidParams, 'name must be a non-empty string');
|
|
22
|
+
}
|
|
23
|
+
let detail;
|
|
24
|
+
try {
|
|
25
|
+
detail = await provider.getExerciseDetail(name.trim());
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err instanceof RangeError) {
|
|
29
|
+
throw new McpError(ErrorCode.InvalidParams, err.message);
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: 'text', text: JSON.stringify(detail, null, 2) }],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = dirname(__filename);
|
|
6
|
+
export const SURFACE_PREVIEW_TOOL = {
|
|
7
|
+
name: 'surface_preview',
|
|
8
|
+
description: 'Renders a RunsNative component fixture (run-table + run-button) inside an MCP App ' +
|
|
9
|
+
'iframe. Used to verify that RunsNative web components hydrate correctly in the claude.ai ' +
|
|
10
|
+
'MCP Apps sandbox. Returns HTML content. This tool exists for RUN-477 verification — ' +
|
|
11
|
+
'it is not a production surface.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {},
|
|
15
|
+
required: [],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export function handleSurfacePreview() {
|
|
19
|
+
// Read fixture from the fixtures/ sibling dir at call time, not compile time,
|
|
20
|
+
// so hot-reloading the fixture doesn't require rebuilding the server.
|
|
21
|
+
// In dist/, __dirname resolves to dist/tools/; fixture is at ../../fixtures/.
|
|
22
|
+
const fixturePath = resolve(__dirname, '../../fixtures/surface-preview.html');
|
|
23
|
+
const html = readFileSync(fixturePath, 'utf8');
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: 'resource',
|
|
28
|
+
resource: {
|
|
29
|
+
uri: 'data:text/html;charset=utf-8,surface-preview',
|
|
30
|
+
mimeType: 'text/html',
|
|
31
|
+
text: html,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@runsnative/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist/"
|
|
7
|
+
],
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"runsnative-mcp": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"http-server": "node dist/http-server.js",
|
|
22
|
+
"validate": "node dist/test/validate.js",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.6.0",
|
|
30
|
+
"playwright": "^1.58.2",
|
|
31
|
+
"sharp": "^0.33.0",
|
|
32
|
+
"typescript": "^5.5.3",
|
|
33
|
+
"vitest": "^4.1.2",
|
|
34
|
+
"zod": "*"
|
|
35
|
+
}
|
|
36
|
+
}
|