@ehrocks/fe-mcp-server 1.0.6 → 1.0.8
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 +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +109 -85
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ An MCP server for searching Hero Design System components.
|
|
|
22
22
|
## Troubleshooting
|
|
23
23
|
- MCP connection gets `Client closed` error: *Ensure that Node.js is accessible globally.* Example: If you're using `asdf`, you can set the default Node.js version with:
|
|
24
24
|
```
|
|
25
|
-
asdf set -u nodejs
|
|
25
|
+
asdf set -u nodejs 22.22.0
|
|
26
26
|
```
|
|
27
27
|
- Connect error or timeout: Because our MCP service is deployed to Employment Hero staging cluster, ensure you have the connection via VPN. [Read more](https://employmenthero.atlassian.net/wiki/spaces/PLAT/pages/1648525859/VPN+-+Internal+services+and+sandbox+accessibility)
|
|
28
28
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
export interface TextContent {
|
|
3
3
|
[key: string]: unknown;
|
|
4
|
-
type:
|
|
4
|
+
type: "text";
|
|
5
5
|
text: string;
|
|
6
6
|
}
|
|
7
7
|
export interface HeroDesignComponentExample {
|
|
@@ -18,4 +18,10 @@ export interface HeroDesignComponentExample {
|
|
|
18
18
|
similarity_score: number;
|
|
19
19
|
matched_embedding_type: string;
|
|
20
20
|
}
|
|
21
|
-
export declare const searchHeroDesignComponentExamples: (query: string, limit?: number, threshold?: number) => Promise<
|
|
21
|
+
export declare const searchHeroDesignComponentExamples: (query: string, limit?: number, threshold?: number) => Promise<{
|
|
22
|
+
ok: true;
|
|
23
|
+
content: TextContent[];
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
message: string;
|
|
27
|
+
}>;
|
package/dist/index.js
CHANGED
|
@@ -1,120 +1,144 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { version } = require("../package.json");
|
|
8
|
+
const HERO_DESIGN_COMPONENT_EXAMPLES_HOST = process.env.ENVIRONMENT === "LOCAL"
|
|
9
|
+
? "http://localhost:8000"
|
|
10
|
+
: "https://fe-mcp-service.staging.ehrocks.com";
|
|
11
|
+
const TIMEOUT_MS = 15000;
|
|
12
|
+
const IS_LOCAL = process.env.ENVIRONMENT === "LOCAL";
|
|
13
|
+
const CONNECTION_ERROR = IS_LOCAL
|
|
14
|
+
? "fe-mcp-service is unreachable. Is the local service running?"
|
|
15
|
+
: "fe-mcp-service is unreachable. VPN connection is required.";
|
|
16
|
+
const TIMEOUT_ERROR = IS_LOCAL
|
|
17
|
+
? "fe-mcp-service timed out. Is the local service running?"
|
|
18
|
+
: "fe-mcp-service timed out. Check VPN. If already connected, the service may be degraded — retry.";
|
|
19
|
+
const classifyError = (err) => {
|
|
20
|
+
if (err instanceof Error && err.name === "TimeoutError")
|
|
21
|
+
return TIMEOUT_ERROR;
|
|
22
|
+
if (err instanceof TypeError && err.message === "fetch failed")
|
|
23
|
+
return CONNECTION_ERROR;
|
|
24
|
+
return `An error occurred while processing your request: ${err}`;
|
|
25
|
+
};
|
|
26
|
+
const fetchWithTimeout = async (url, body) => {
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body,
|
|
31
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`HTTP error status: ${response.status}`);
|
|
35
|
+
}
|
|
36
|
+
return response.json();
|
|
37
|
+
};
|
|
38
|
+
const buildContent = (examples) => {
|
|
39
|
+
if (!examples.length) {
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: "I couldn't find any relevant component examples for your query.",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const responseParts = [];
|
|
51
|
+
examples.forEach((example, index) => {
|
|
52
|
+
responseParts.push(`\n${index + 1}. ${example.component_name} - ${example.example_name}`, `\nSimilarity Score: ${example.similarity_score.toFixed(2)}`, `\nComponent Description: ${example.component_description}`);
|
|
53
|
+
if (example.purpose_description) {
|
|
54
|
+
responseParts.push(`\nPurpose: ${example.purpose_description}`);
|
|
55
|
+
}
|
|
56
|
+
if (example.technical_description) {
|
|
57
|
+
responseParts.push(`\nTechnical Details: ${example.technical_description}`);
|
|
17
58
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return [{
|
|
21
|
-
type: 'text',
|
|
22
|
-
text: "I couldn't find any relevant component examples for your query."
|
|
23
|
-
}];
|
|
59
|
+
if (example.code) {
|
|
60
|
+
responseParts.push(`\nCode Example:\n\`\`\`jsx\n${example.code}\n\`\`\``);
|
|
24
61
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
if (example.props_description) {
|
|
63
|
+
responseParts.push(`\nProps Description: ${example.props_description}`);
|
|
64
|
+
}
|
|
65
|
+
if (example.matched_embedding_type) {
|
|
66
|
+
responseParts.push(`\nMatched Embedding Type: ${example.matched_embedding_type}`);
|
|
67
|
+
}
|
|
68
|
+
responseParts.push("\n" + "=".repeat(80) + "\n");
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: "Here are the most relevant component examples I found:\n" +
|
|
76
|
+
responseParts.join("\n"),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export const searchHeroDesignComponentExamples = async (query, limit = 5, threshold = 0.4) => {
|
|
82
|
+
const url = `${HERO_DESIGN_COMPONENT_EXAMPLES_HOST}/retrieve_hero_design`;
|
|
83
|
+
const body = JSON.stringify({ query, limit, threshold });
|
|
84
|
+
try {
|
|
85
|
+
return buildContent(await fetchWithTimeout(url, body));
|
|
49
86
|
}
|
|
50
|
-
catch (
|
|
51
|
-
return
|
|
52
|
-
type: 'text',
|
|
53
|
-
text: `An error occurred while processing your request: ${error}`
|
|
54
|
-
}];
|
|
87
|
+
catch (err) {
|
|
88
|
+
return { ok: false, message: classifyError(err) };
|
|
55
89
|
}
|
|
56
90
|
};
|
|
57
91
|
async function startServer() {
|
|
58
|
-
const server = new McpServer({
|
|
59
|
-
|
|
60
|
-
version: "1.0.0"
|
|
92
|
+
const server = new McpServer({ name: "hero-design-mcp-server", version }, {
|
|
93
|
+
instructions: "Invoke for any @hero-design/react implementation task: selecting the right component for a UI need, retrieving JSX code examples, or exploring design patterns and design system resources.",
|
|
61
94
|
});
|
|
62
|
-
server.tool("search_hero_design_component_examples", `
|
|
95
|
+
server.tool("search_hero_design_component_examples", `Retrieve Hero Design System examples, patterns, and references using natural language.
|
|
63
96
|
|
|
64
|
-
|
|
97
|
+
Describe the UI behavior, component, or pattern you need. Covers component selection guidance, JSX code examples, design patterns, and use cases across @hero-design/react. Not a prop API reference — use the component library docs for precise prop signatures.
|
|
65
98
|
|
|
66
|
-
|
|
67
|
-
2. Code Examples: Specifically searches through code implementations and syntax
|
|
68
|
-
3. Component Descriptions: Focuses on component usage descriptions and explanations
|
|
69
|
-
4. Purpose & Use Cases: Matches queries about component purposes and intended use cases
|
|
70
|
-
5. Technical Details: Searches through technical implementation details and specifications
|
|
71
|
-
6. Props & Configuration: Finds examples based on prop usage and configuration options
|
|
72
|
-
7. Design Patterns: Matches React patterns and best practices demonstrated in examples
|
|
73
|
-
8. Use Case Scenarios: Searches through real-world application scenarios
|
|
99
|
+
Common queries: button loading state, form validation error, dropdown with search, empty state, date picker range, modal confirmation dialog, toast notification.
|
|
74
100
|
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
- Search for specific use cases (e.g., "loading state in forms")
|
|
81
|
-
- Find technical implementations (e.g., "async data loading pattern")
|
|
82
|
-
|
|
83
|
-
The tool will return the most relevant examples, including:
|
|
84
|
-
- Component name and description
|
|
85
|
-
- Example code with explanations
|
|
86
|
-
- Technical details and implementation notes
|
|
87
|
-
- Purpose and use case descriptions
|
|
88
|
-
- Props and configuration details
|
|
89
|
-
- Design patterns and best practices`, {
|
|
101
|
+
Returns for each match:
|
|
102
|
+
- Component name and similarity score
|
|
103
|
+
- Component and example descriptions
|
|
104
|
+
- Code example (JSX)
|
|
105
|
+
- Technical details, design patterns, and props context when available`, {
|
|
90
106
|
query: z
|
|
91
107
|
.string()
|
|
108
|
+
.trim()
|
|
109
|
+
.min(1, "query must not be empty")
|
|
92
110
|
.describe("The search query describing what you're looking for in the component examples. Be specific about what aspects you're interested in (code, usage, props, patterns, etc.)"),
|
|
93
111
|
limit: z
|
|
94
112
|
.string()
|
|
95
|
-
.transform(val => {
|
|
113
|
+
.transform((val) => {
|
|
96
114
|
const num = Number(val);
|
|
97
|
-
if (isNaN(num)
|
|
115
|
+
if (isNaN(num))
|
|
98
116
|
return 5;
|
|
99
|
-
return num;
|
|
117
|
+
return Math.min(Math.max(Math.round(num), 1), 5);
|
|
100
118
|
})
|
|
101
119
|
.optional()
|
|
102
120
|
.default("5")
|
|
103
|
-
.describe("Maximum number of distinct component examples to return"),
|
|
121
|
+
.describe("Maximum number of distinct component examples to return (1–5)"),
|
|
104
122
|
threshold: z
|
|
105
123
|
.string()
|
|
106
|
-
.transform(val => {
|
|
124
|
+
.transform((val) => {
|
|
107
125
|
const num = parseFloat(val);
|
|
108
|
-
if (isNaN(num)
|
|
126
|
+
if (isNaN(num))
|
|
109
127
|
return 0.4;
|
|
110
|
-
return num;
|
|
128
|
+
return Math.min(Math.max(num, 0), 0.99);
|
|
111
129
|
})
|
|
112
130
|
.optional()
|
|
113
131
|
.default("0.4")
|
|
114
|
-
.describe("Minimum similarity score (0.0 to
|
|
132
|
+
.describe("Minimum similarity score (0.0 to 0.99) for returned examples. Lower values return more results but might be less relevant"),
|
|
115
133
|
}, async ({ query, limit, threshold }) => {
|
|
116
|
-
const
|
|
117
|
-
|
|
134
|
+
const result = await searchHeroDesignComponentExamples(query, limit, threshold);
|
|
135
|
+
if (result.ok) {
|
|
136
|
+
return { content: result.content };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
content: [{ type: "text", text: result.message }],
|
|
140
|
+
isError: true,
|
|
141
|
+
};
|
|
118
142
|
});
|
|
119
143
|
const transport = new StdioServerTransport();
|
|
120
144
|
await server.connect(transport);
|