x
This commit is contained in:
1 parent
724c4c66c9
commit
4d5db0c9dd
11 files changed
+644
-38
No files matched your search
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
@@ -0,0 +1,106 @@
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
||||
@@ -0,0 +1,15 @@
|
||||
# mcp-server
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.13. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
@@ -0,0 +1,220 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import axios from "axios";
|
||||
import FormData from "form-data";
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:3000/api";
|
||||
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "admin";
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "homaroma123";
|
||||
|
||||
let authToken = "";
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: BACKEND_URL,
|
||||
});
|
||||
|
||||
api.interceptors.request.use(async (config) => {
|
||||
if (!authToken && config.url !== "/auth/login") {
|
||||
console.error("[MCP] No token found. Attempting auto-login...");
|
||||
await performLogin();
|
||||
}
|
||||
if (authToken) {
|
||||
config.headers.Authorization = `Bearer ${authToken}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
async function performLogin() {
|
||||
try {
|
||||
const res = await axios.post(`${BACKEND_URL}/auth/login`, {
|
||||
username: ADMIN_USERNAME,
|
||||
password: ADMIN_PASSWORD,
|
||||
});
|
||||
if (res.data && res.data.token) {
|
||||
authToken = res.data.token;
|
||||
console.error("[MCP] Successfully logged in to Backend API.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("[MCP] Auto-login failed:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "homaroma-mcp",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "ping_backend",
|
||||
description: "Ping the backend to verify connection and authentication",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
{
|
||||
name: "search_products",
|
||||
description: "Search for products and get their basic info",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
search: { type: "string", description: "Search term" },
|
||||
limit: { type: "number", description: "Max results" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "get_product_detail",
|
||||
description: "Get full product details including variants, images, and category",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "number", description: "Product ID" }
|
||||
},
|
||||
required: ["id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "update_product_content",
|
||||
description: "Update product basic content (name, description, details)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "number" },
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
details: { type: "string" },
|
||||
},
|
||||
required: ["id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "upload_product_image_from_url",
|
||||
description: "Download image from URL, upload to backend, and append to product images",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
productId: { type: "number" },
|
||||
imageUrl: { type: "string", description: "URL of the image to download" }
|
||||
},
|
||||
required: ["productId", "imageUrl"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "generate_mock_review",
|
||||
description: "Create a dummy review for a product",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
productId: { type: "number" },
|
||||
rating: { type: "number", description: "1-5" },
|
||||
comment: { type: "string" },
|
||||
customerName: { type: "string", description: "Name of the mock reviewer" }
|
||||
},
|
||||
required: ["productId", "rating", "customerName"]
|
||||
}
|
||||
}
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const params = request.params.arguments as any;
|
||||
|
||||
try {
|
||||
switch (request.params.name) {
|
||||
case "ping_backend": {
|
||||
const res = await api.get("/products?limit=1");
|
||||
return { content: [{ type: "text", text: `Connection successful!` }] };
|
||||
}
|
||||
case "search_products": {
|
||||
const search = params.search ? `&search=${encodeURIComponent(params.search)}` : "";
|
||||
const limit = params.limit ? `&limit=${params.limit}` : "&limit=10";
|
||||
const res = await api.get(`/products?${search}${limit}`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
case "get_product_detail": {
|
||||
const res = await api.get(`/products/${params.id}`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
case "update_product_content": {
|
||||
const payload: any = {};
|
||||
if (params.name) payload.name = params.name;
|
||||
if (params.description) payload.description = params.description;
|
||||
if (params.details) payload.details = params.details;
|
||||
|
||||
const res = await api.patch(`/products/${params.id}`, payload);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
case "upload_product_image_from_url": {
|
||||
// 1. Download image
|
||||
const imgRes = await axios.get(params.imageUrl, { responseType: 'arraybuffer' });
|
||||
const buffer = Buffer.from(imgRes.data, 'binary');
|
||||
const ext = params.imageUrl.split('.').pop()?.split('?')[0] || 'jpg';
|
||||
|
||||
// 2. Upload to backend
|
||||
const formData = new FormData();
|
||||
formData.append('file', buffer, { filename: `download.${ext}` });
|
||||
|
||||
const uploadRes = await api.post('/upload', formData, {
|
||||
headers: formData.getHeaders()
|
||||
});
|
||||
const uploadedUrl = uploadRes.data.url;
|
||||
|
||||
// 3. Get current product to append image
|
||||
const prodRes = await api.get(`/products/${params.productId}`);
|
||||
const product = prodRes.data;
|
||||
const newImages = [...product.images, { url: uploadedUrl, order: product.images.length }];
|
||||
|
||||
// 4. Update product (using PUT to sync variants and images)
|
||||
// Ensure we pass required fields
|
||||
const putRes = await api.put(`/products/${params.productId}`, {
|
||||
categoryId: product.categoryId,
|
||||
description: product.description,
|
||||
details: product.details,
|
||||
variants: product.variants,
|
||||
images: newImages
|
||||
});
|
||||
|
||||
return { content: [{ type: "text", text: `Image uploaded and product updated. New image URL: ${uploadedUrl}` }] };
|
||||
}
|
||||
case "generate_mock_review": {
|
||||
const res = await api.post("/reviews", {
|
||||
productId: params.productId,
|
||||
rating: params.rating,
|
||||
comment: params.comment,
|
||||
customerName: params.customerName
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
||||
return {
|
||||
content: [{ type: "text", text: `Error executing tool: ${errorMsg}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
await performLogin();
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("HomAroma MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "mcp-server",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"axios": "^1.17.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"form-data": "^4.0.5"
|
||||
}
|
||||
}
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /home/kitisak/Projects/homaroma/apps/mcp-server
|
||||
bun run index.ts
|
||||
@@ -0,0 +1,2 @@
|
||||
[MCP] Successfully logged in to Backend API.
|
||||
HomAroma MCP Server running on stdio
|
||||
Whitespace-only changes.
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"types": ["bun"],
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user