feat:add openapi to mcp sdk. (#5626)

* feat:add openapi to mcp sdk.

# Conflicts:
#	frontend/pnpm-lock.yaml

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* feat:add openapi to mcp sdk.

* fix devbox.json

* fix dockerfile

* fix dockerfile

* fix bug.

* fix bug.

* fix bug.

* fix bug.

* fix bug.

* fix openapi.json

* fix bug.

* fix bug.

* fix openapi bug.

* demo

* fix openapi bug.

* fix openapi bug.

* fix openapi bug.

* fix ci

* update

* fix build-packages

* update path

* update

* fix bug.

* fix bug.

* fix ci bug.

* fix ci bug.

* fix ci bug.

* fix ci bug.

* fix ci bug.

---------

Co-authored-by: zjy <3161362058@qq.com>
This commit is contained in:
Bearix
2025-06-23 17:34:36 +08:00
committed by GitHub
co-authored by zjy
parent a1e8f76614
commit 1e6d1b951a
17 changed files with 1696 additions and 165 deletions
+1
View File
@@ -94,6 +94,7 @@ RUN if ["$name" = "objectstorage" ]; then \
COPY --from=builder /app/$path/public ./$path/public
COPY --from=builder --chown=nextjs:nodejs /app/$path/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/$path/.next/static ./$path/.next/static
EXPOSE 3000
ENV PORT=3000
+1 -1
View File
@@ -18,7 +18,7 @@
"gen:theme-typings": "pnpm chakra-cli tokens packages/ui/src/theme/theme.ts --out node_modules/.pnpm/node_modules/@chakra-ui/styled-system/dist/theming.types.d.ts",
"postinstall": "pnpm run gen:theme-typings && pnpm run build-packages",
"prepare": "cd .. && husky frontend/.husky",
"build-packages": "pnpm -r --filter ./packages/client-sdk run build"
"build-packages": "pnpm -r --filter './packages/*' run build"
},
"workspaces": [
"./packages/*",
+1 -1
View File
@@ -7,7 +7,7 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "next dev",
"build": "echo \"build driver\""
"build": "exit 0"
},
"keywords": [
"driver",
+2
View File
@@ -0,0 +1,2 @@
node_modules
dist
+54
View File
@@ -0,0 +1,54 @@
{
"name": "sealos-mcp-sdk",
"version": "0.1.1",
"description": "sealos mcp sdk for OpenAPI integration with MCP",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup -c",
"dev": "rollup -c -w"
},
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts"
]
}
},
"keywords": [
"sealos",
"mcp",
"sdk",
"openapi",
"vercel"
],
"author": "lizhenq2009@gmail.com",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.4",
"@types/node": "^20.7.1",
"rollup": "2.79.1",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-dts": "^4.2.3",
"tslib": "^2.6.2",
"typescript": "^5.0.0"
},
"dependencies": {
"@vercel/mcp-adapter": "^0.9.1",
"axios": "^1.5.1",
"https": "^1.0.0",
"openapi-types": "^12.1.3",
"next": "^13.5.3",
"zod": "^3.22.0"
},
"peerDependencies": {
"@kubernetes/client-node": "^0.18.1"
}
}
@@ -0,0 +1,70 @@
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
import copy from 'rollup-plugin-copy';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
const external = [
'axios',
'https',
'zod',
'path',
'fs',
'@vercel/mcp-adapter',
'async_hooks',
'openapi-types'
];
const createConfig = (input, output, plugins = []) => ({
input,
output,
external,
plugins: [
nodeResolve(),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
compilerOptions: {
declaration: false,
declarationMap: false,
noImplicitAny: false
},
noEmitOnError: false
}),
...plugins
]
});
const createDtsConfig = (input, output, plugins = []) => ({
input,
output,
external,
plugins: [dts(), ...plugins]
});
export default [
createConfig('src/index.ts', [
{
file: 'dist/index.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/index.esm.js',
format: 'es',
sourcemap: true
}
]),
createDtsConfig('src/index.ts', {
file: 'dist/index.d.ts',
format: 'es'
}, [
copy({
targets: [
{ src: 'package.json', dest: 'dist' },
{ src: 'README.md', dest: 'dist', failOnError: false }
]
})
])
];
@@ -0,0 +1,111 @@
import axios from 'axios';
import https from 'https';
import { z } from 'zod';
export async function executeHttpRequest(
toolId: string,
toolName: string,
params: any,
apiBaseUrl: string,
headers: Record<string, string> = {}
): Promise<{ content: Array<{ type: string; text: string }> }> {
console.error(`Executing tool: ${toolId} (${toolName})`);
// Only keep authentication-related headers
const cleanedHeaders: Record<string, string> = {};
// Preserve authorization-related headers
if (headers.authorization) {
cleanedHeaders.Authorization = headers.authorization;
}
if (headers.Authorization) {
cleanedHeaders.Authorization = headers.Authorization;
}
// Output cleaned headers
console.log(`Transformed headers: ${JSON.stringify(cleanedHeaders, null, 2)}`);
try {
const [method, ...pathParts] = toolId.split('-');
const path = '/' + pathParts.join('/').replace(/-/g, '/');
const baseUrl = apiBaseUrl.endsWith('/') ? apiBaseUrl : `${apiBaseUrl}/`;
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
const url = new URL(cleanPath, baseUrl).toString();
const config: any = {
method: method.toLowerCase(),
url: url,
headers: cleanedHeaders
};
if (method.toLowerCase() === 'get') {
if (params && typeof params === 'object') {
const queryParams: Record<string, string> = {};
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
queryParams[key] = value.join(',');
} else if (value !== undefined && value !== null) {
queryParams[key] = String(value);
}
}
config.params = queryParams;
}
} else {
config.data = params;
}
console.error(`Processed parameters:`, config.params || config.data);
console.error('Final request config:', config);
const response = await axios(config);
console.error('Response status:', response.status);
console.error('Response headers:', response.headers);
console.error('Response data:', response.data);
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2)
}
]
};
} catch (error) {
if (axios.isAxiosError(error)) {
console.error('Request failed:', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
headers: error.response?.headers
});
throw new Error(
`API request failed: ${error.message} - ${JSON.stringify(error.response?.data)}`
);
}
throw error;
}
}
export function convertToZodProperties(schema: {
type: string;
properties: Record<string, any>;
required?: string[];
}): z.ZodRawShape {
const zodProperties: z.ZodRawShape = {};
for (const [key, prop] of Object.entries(schema.properties)) {
switch (prop.type) {
case 'string':
zodProperties[key] = z.string();
break;
case 'number':
zodProperties[key] = z.number();
break;
case 'boolean':
zodProperties[key] = z.boolean();
break;
case 'integer':
zodProperties[key] = z.number().int();
default:
zodProperties[key] = z.any();
}
if (!schema.required?.includes(key)) {
zodProperties[key] = zodProperties[key].optional();
}
}
return zodProperties;
}
+1
View File
@@ -0,0 +1 @@
export { McpHandler, createMcpApiHandler } from './sdk';
+111
View File
@@ -0,0 +1,111 @@
// openapi-tools.ts
import { OpenAPIV3 } from 'openapi-types';
import path from 'path';
import * as fs from 'fs';
export interface Tool {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, any>;
required?: string[];
};
}
function getOpenApiSpecPath(specPath: any) {
if (typeof specPath !== 'string') {
throw new Error('Invalid OpenAPI spec path');
}
const normalizedPath = path.normalize(specPath).replace(/^(\.\.(\/|\\|$))+/, '');
const fullPath = path.resolve(process.cwd(), normalizedPath);
const baseDir = process.cwd();
if (!fullPath.startsWith(baseDir)) {
throw new Error('Access to paths outside the base directory is not allowed');
}
if (!fs.existsSync(fullPath)) {
throw new Error(`OpenAPI spec file not found: ${normalizedPath}`);
}
return fullPath;
}
export class OpenAPIToolsParser {
public static loadAndParseOpenAPISpec(config: {
openApiSpec: string | OpenAPIV3.Document;
}): Map<string, Tool> {
const tools = new Map<string, Tool>();
let spec: OpenAPIV3.Document;
try {
const resolvedPath = getOpenApiSpecPath(config.openApiSpec);
const content = fs.readFileSync(resolvedPath, 'utf-8');
spec = JSON.parse(content);
} catch (error) {
// @ts-ignore
throw new Error(`Failed to parse OpenAPI spec: ${error.message}`);
}
// @ts-ignore
for (const [path, pathItem] of Object.entries(spec.paths)) {
if (!pathItem) continue;
for (const [method, operation] of Object.entries(pathItem)) {
if (method === 'parameters' || !operation) continue;
const op = operation as OpenAPIV3.OperationObject;
const cleanPath = path.replace(/^\//, '');
const toolId = `${method.toUpperCase()}-${cleanPath}`.replace(/[^a-zA-Z0-9-]/g, '-');
const tool: Tool = {
// @ts-ignore
name: op.summary,
description: op.description || 'No description available',
inputSchema: {
type: 'object',
properties: {},
required: undefined
}
};
if (op.parameters) {
for (const param of op.parameters) {
if ('name' in param && 'in' in param) {
const paramSchema = param.schema as OpenAPIV3.SchemaObject;
tool.inputSchema.properties[param.name] = {
type: paramSchema?.type || 'string',
description: param.description || `${param.name} parameter`
};
if (param.required) {
tool.inputSchema.required = tool.inputSchema.required || [];
tool.inputSchema.required.push(param.name);
}
}
}
}
if (op.requestBody) {
const requestBody = op.requestBody as OpenAPIV3.RequestBodyObject;
if (requestBody.content) {
const jsonContent = requestBody.content['application/json'];
if (jsonContent && jsonContent.schema) {
const schema = jsonContent.schema as OpenAPIV3.SchemaObject;
if (schema.properties) {
for (const [propName, propSchema] of Object.entries(schema.properties)) {
const propSchemaObj = propSchema as OpenAPIV3.SchemaObject;
tool.inputSchema.properties[propName] = {
type: propSchemaObj.type || 'string',
description: propSchemaObj.description || `${propName} property`
};
}
}
if (schema.required) {
tool.inputSchema.required = tool.inputSchema.required || [];
tool.inputSchema.required = [...tool.inputSchema.required, ...schema.required];
}
}
}
}
tools.set(toolId, tool);
}
}
return tools;
}
}
export type { OpenAPIV3 };
+111
View File
@@ -0,0 +1,111 @@
import { createMcpHandler } from '@vercel/mcp-adapter';
import { OpenAPIToolsParser } from './openapi';
import { executeHttpRequest, convertToZodProperties } from './httprequest';
import { NextApiRequest, NextApiResponse } from 'next';
import { AsyncLocalStorage } from 'async_hooks';
export function McpHandler(path: string, url: string) {
const requestContextStorage = new AsyncLocalStorage();
const tools = OpenAPIToolsParser.loadAndParseOpenAPISpec({
openApiSpec: path
});
const baseHandler = createMcpHandler(
(server) => {
tools.forEach((tool, id) => {
const zodProperties = convertToZodProperties(tool.inputSchema);
server.tool(tool.name, tool.description, zodProperties, async (params: any) => {
const context = requestContextStorage.getStore();
// @ts-ignore
const headers = context ? context.headers : {};
const createUserResult = await executeHttpRequest(id, tool.name, params, url, headers);
return {
content: [
{
type: 'text',
text: JSON.stringify(createUserResult)
}
]
};
});
});
},
{
capabilities: {
tools: {
echo: {
description: 'Echo a message'
}
}
}
},
{
redisUrl: process.env.REDIS_URL,
basePath: '/api',
verboseLogs: true,
maxDuration: 60
}
);
return async function (request: Request): Promise<Response> {
// Extract request headers
const headersObj: Record<string, string> = {};
request.headers.forEach((value, key) => {
headersObj[key] = value;
});
// Create request context
const requestContext = {
headers: headersObj
};
// Run request handler with async local storage
return await requestContextStorage.run(requestContext, async () => {
try {
return await baseHandler(request);
} catch (error) {
console.error('Error processing request:', error);
return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
});
};
}
export function createMcpApiHandler(path: string, baseUrl: string) {
return async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const protocol = req.headers['x-forwarded-proto'] || 'http';
const host = req.headers.host;
const url = new URL(req.url || '', `${protocol}://${host}`);
const request = new Request(url.toString(), {
method: req.method || 'GET',
headers: new Headers(req.headers as Record<string, string>),
body:
req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH'
? JSON.stringify(req.body)
: undefined
});
const handler = McpHandler(path, baseUrl);
const response = await handler(request);
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
res.status(response.status);
if (response.body) {
const responseData = await response.text();
// nosemgrep: javascript.express.security.audit.xss.direct-response-write.direct-response-write
return res.send(responseData);
} else {
return res.end();
}
} catch (error: any) {
console.error('MCP Handler Error:', error);
return res.status(500).json({
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error?.message : undefined
});
}
};
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.deps.json",
"compilerOptions": {
"lib": ["es2017", "DOM"],
"noImplicitAny": true,
"declaration": true,
"moduleResolution": "node",
"esModuleInterop": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "dist/**/*.d.ts", "dist/**/*.ts"]
}
+1 -1
View File
@@ -7,7 +7,7 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "next dev",
"build": "echo \"build @sealos/ui\""
"build": "exit 0"
},
"keywords": [],
"author": "",
+771 -155
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
import { McpHandler } from 'sealos-mcp-sdk';
import path from 'path';
export const dynamic = 'force-dynamic';
const handler = McpHandler(
path.join(process.cwd(), 'public', 'devbox.json'),
'http://devbox-frontend.devbox-frontend.svc.cluster.local:3000'
);
export { handler as GET, handler as POST, handler as DELETE };
+2 -1
View File
@@ -31,13 +31,13 @@
"fast-json-patch": "^3.1.1",
"framer-motion": "^10.16.4",
"immer": "^10.1.1",
"lucide-react": "^0.476.0",
"ini": "^4.1.3",
"js-cookie": "^3.0.5",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"lucide-react": "^0.476.0",
"nanoid": "^4.0.2",
"next": "14.2.5",
"next-intl": "^3.17.2",
@@ -51,6 +51,7 @@
"react-syntax-highlighter": "^15.5.0",
"sass": "^1.68.0",
"sealos-desktop-sdk": "workspace:*",
"sealos-mcp-sdk": "workspace:^",
"zod": "^3.23.8",
"zod-openapi": "^4.2.4",
"zustand": "^4.5.4"
@@ -0,0 +1,418 @@
{
"openapi": "3.0.0",
"info": {
"title": "Devbox API",
"version": "1.0.0",
"description": ""
},
"paths": {
"/api/v1/DevBox/create": {
"post": {
"summary": "createDevbox",
"description": "使用模版,创建一个新的DevBox",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "DevBox名称"
},
"runtimeName": {
"type": "string",
"enum": [
"Debian",
"C++",
"Rust",
"Java",
"Go",
"Python",
"Node.js",
".Net",
"C",
"PHP"
],
"description": "DevBox 创建所使用的模版的名称,有以下可选项:Debian,C++,Rust,Java,Go,Python,Node.js,.Net,C,PHP"
},
"cpu": {
"type": "number",
"minimum": 0,
"default": 2000,
"description": "CPU核心数, 使用1000,2000的格式,代表1Core, 2Core"
},
"memory": {
"type": "number",
"minimum": 0,
"default": 4096,
"description": "内存数, 使用2048, 4096,代表2G, 4G"
}
},
"required": [
"name",
"runtimeName"
]
}
}
}
},
"responses": {
"200": {
"description": "Devbox created successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Devbox name"
},
"sshPort": {
"type": "number",
"description": "SSH port"
},
"base64PrivateKey": {
"type": "string",
"description": "Base64 encoded private key"
},
"userName": {
"type": "string",
"description": "User name"
},
"workingDir": {
"type": "string",
"description": "Working directory"
},
"domain": {
"type": "string",
"description": "Domain"
}
},
"required": [
"name",
"sshPort",
"base64PrivateKey",
"userName",
"workingDir",
"domain"
]
}
},
"required": [
"data"
]
}
}
}
},
"400": {
"description": "Invalid request body",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
},
"data": {
"type": "string"
}
},
"required": [
"code",
"message"
]
}
}
}
},
"404": {
"description": "Template not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
},
"data": {
"type": "string"
}
},
"required": [
"code",
"message"
]
}
}
}
},
"409": {
"description": "Devbox already exists",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
},
"data": {
"type": "string"
}
},
"required": [
"code",
"message"
]
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
},
"data": {
"type": "string"
}
},
"required": [
"code",
"message"
]
}
}
}
}
}
}
},
"/api/v1/DevBox/release": {
"post": {
"summary": "releaseDevbox",
"description": "DevBox发版,release,为现有的DevBox发布一个新的版本",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"devboxName": {
"type": "string",
"minLength": 1,
"description": "DevBox名称"
},
"tag": {
"type": "string",
"minLength": 1,
"description": "DevBox新的Tag"
},
"releaseDes": {
"type": "string",
"default": "",
"description": "新版本的介绍"
}
},
"required": [
"devboxName",
"tag"
]
}
}
}
}
}
},
"/api/v1/DevBox/releases": {
"get": {
"summary": "getDevboxreleases",
"description": "获取单个Devbox的所有历史版本",
"parameters": [
{
"in": "query",
"name": "devboxName",
"description": "Devbox名称",
"schema": {
"type": "string",
"minLength": 1,
"description": "Devbox名称"
},
"required": true
}
]
}
},
"/api/v1/DevBox/lifecycle": {
"post": {
"summary": "manageDevbox",
"description": "操作Devbox,Devbox生命周期管理,主要包含开机,关机,重启,冷关机,这些操作都通过这个工具完成",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"devboxName": {
"type": "string",
"minLength": 1,
"description": "Devbox名称"
},
"action": {
"type": "string",
"enum": [
"start",
"stop",
"restart",
"shutdown"
],
"description": "操作方式:start为开机,stop为关机,restart为重启,shutdown为冷关机"
}
},
"required": [
"devboxName",
"action"
]
}
}
}
},
"/api/v1/getDevboxListEasyResponse": {
"get": {
"tags": [
"Query"
],
"summary": "获取所有Devbox",
"description": "获取所有Devbox,得到Devbox的列表,可以知道当前有哪些Devbox"
}
}
}
},
"/api/v1/DevBox/ports/create": {
"post": {
"summary": "createPort",
"description": "创建Devbox端口,为现有的Devbox创建一个新的端口",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"devboxName": {
"type": "string",
"minLength": 1,
"description": "Devbox名称"
},
"port": {
"type": "number",
"minimum": 1,
"maximum": 65535,
"description": "新创建端口的端口号"
},
"protocol": {
"type": "string",
"enum": [
"HTTP",
"GRPC",
"WS"
],
"default": "HTTP",
"description": "这个端口的通信方式,有三种通信方式可选:HTTP,GRPC,WS"
}
},
"required": [
"devboxName",
"port"
]
}
}
}
}
}
},
"/api/deployDevbox": {
"post": {
"summary": "deployDevbox",
"description": "部署 Devbox",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"devboxName": {
"type": "string",
"minLength": 1,
"description": "Devbox名称"
},
"port": {
"type": "number",
"minimum": 1,
"default": 65535,
"description": "端口号"
},
"tag": {
"type": "string",
"minLength": 1,
"description": "Devbox版本号,即Devbox发布版本时所使用的版本号"
},
"cpu": {
"type": "number",
"minimum": 0,
"default": 2000,
"description": "CPU核心数,使用1000、2000,分别代表1核、2核"
},
"memory": {
"type": "number",
"minimum": 0,
"default": 4096,
"description": "内存大小,使用2048、4096,分别代表2G、4G"
}
},
"required": [
"devboxName",
"tag"
]
}
}
}
}
}
},
"/api/v1/DevBox/list": {
"get": {
"tags": [
"Query"
],
"summary": "getDevboxList",
"description": "获取DevBox列表,获得每个DevBox的详细信息"
}
}
}
}
+19 -6
View File
@@ -1,12 +1,21 @@
{
"extends": "../../tsconfig.web.json",
"compilerOptions": {
"baseUrl": ".",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "ES2020",
"forceConsistentCasingInFileNames": false,
"paths": {
"@/*": ["./*"],
"prisma/*": ["prisma/*"]
"@/*": [
"./*"
],
"prisma/*": [
"prisma/*"
],
},
"plugins": [
{
@@ -14,6 +23,10 @@
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"extends": "../../tsconfig.web.json"
}
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
]
}