Initial commit
This commit is contained in:
53
.github/copilot-instructions.md
vendored
Normal file
53
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# MCP Demo Server - Copilot Instructions
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个 Model Context Protocol (MCP) 服务器示例项目,使用 TypeScript 编写。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **语言**: TypeScript
|
||||
- **运行时**: Node.js
|
||||
- **MCP SDK**: @modelcontextprotocol/sdk
|
||||
- **Schema 验证**: Zod
|
||||
|
||||
## 项目结构
|
||||
|
||||
- `src/index.ts` - MCP 服务器主入口文件
|
||||
- `build/` - 编译后的 JavaScript 文件
|
||||
- `.vscode/mcp.json` - VS Code MCP 服务器配置
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新工具
|
||||
|
||||
使用 `server.registerTool()` 方法注册新工具:
|
||||
|
||||
```typescript
|
||||
server.registerTool(
|
||||
"tool_name",
|
||||
{
|
||||
description: "工具描述",
|
||||
inputSchema: {
|
||||
// 使用 Zod schema 定义参数
|
||||
},
|
||||
},
|
||||
async (params) => {
|
||||
// 实现工具逻辑
|
||||
return {
|
||||
content: [{ type: "text", text: "结果" }],
|
||||
};
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
|
||||
- `npm run build` - 编译 TypeScript
|
||||
- `npm start` - 运行 MCP 服务器
|
||||
- `npm run dev` - 开发模式(监听文件变化)
|
||||
|
||||
## MCP 参考文档
|
||||
|
||||
- SDK 文档: https://github.com/modelcontextprotocol/typescript-sdk
|
||||
- 协议规范: https://modelcontextprotocol.io/
|
||||
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
typescript/build/
|
||||
|
||||
# Go binaries
|
||||
golang/*.exe
|
||||
golang/*.dll
|
||||
golang/*.so
|
||||
golang/*.dylib
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
*.db
|
||||
13
.vscode/mcp.json
vendored
Normal file
13
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"servers": {
|
||||
"mcp-demo-ts": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["typescript/build/index.js"]
|
||||
},
|
||||
"mcp-demo-go": {
|
||||
"type": "stdio",
|
||||
"command": "${workspaceFolder}/golang/mcp-demo-go.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
157
README.md
Normal file
157
README.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# MCP Demo Server
|
||||
|
||||
一个使用 TypeScript 构建的 Model Context Protocol (MCP) 服务器示例,展示了如何创建和注册各种工具。
|
||||
|
||||
## 功能特性
|
||||
|
||||
此 MCP 服务器提供以下示例工具:
|
||||
|
||||
| 工具名称 | 描述 |
|
||||
|---------|------|
|
||||
| `echo` | 回显传入的消息 |
|
||||
| `calculator` | 执行基本数学运算(加、减、乘、除) |
|
||||
| `get_current_time` | 获取当前日期和时间(支持时区) |
|
||||
| `random_number` | 在指定范围内生成随机数 |
|
||||
| `string_utils` | 字符串处理工具(大小写转换、反转、计数等) |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 编译项目
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 运行服务器
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## 在 VS Code 中使用
|
||||
|
||||
项目已配置好 `.vscode/mcp.json`,可以直接在 VS Code 中使用此 MCP 服务器:
|
||||
|
||||
1. 确保已编译项目 (`npm run build`)
|
||||
2. 在 VS Code 中打开此项目
|
||||
3. MCP 服务器会自动被识别并可用
|
||||
|
||||
## 配置示例
|
||||
|
||||
### Claude Desktop 配置
|
||||
|
||||
将以下内容添加到 Claude Desktop 的配置文件中:
|
||||
|
||||
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp-demo": {
|
||||
"command": "node",
|
||||
"args": ["D:\\Projects\\McpDemo\\build\\index.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 工具使用示例
|
||||
|
||||
### Echo 工具
|
||||
```
|
||||
输入: { "message": "Hello, MCP!" }
|
||||
输出: "Echo: Hello, MCP!"
|
||||
```
|
||||
|
||||
### Calculator 工具
|
||||
```
|
||||
输入: { "operation": "add", "a": 10, "b": 5 }
|
||||
输出: "10 add 5 = 15"
|
||||
```
|
||||
|
||||
### Get Current Time 工具
|
||||
```
|
||||
输入: { "timezone": "Asia/Shanghai" }
|
||||
输出: "Current time: 1/16/2026, 2:30:00 PM"
|
||||
```
|
||||
|
||||
### Random Number 工具
|
||||
```
|
||||
输入: { "min": 1, "max": 100 }
|
||||
输出: "Random number between 1 and 100: 42"
|
||||
```
|
||||
|
||||
### String Utils 工具
|
||||
```
|
||||
输入: { "operation": "uppercase", "text": "hello world" }
|
||||
输出: "HELLO WORLD"
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
McpDemo/
|
||||
├── src/
|
||||
│ └── index.ts # MCP 服务器主文件
|
||||
├── build/ # 编译输出目录
|
||||
├── .vscode/
|
||||
│ └── mcp.json # VS Code MCP 配置
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新工具
|
||||
|
||||
在 `src/index.ts` 中使用 `server.registerTool()` 添加新工具:
|
||||
|
||||
```typescript
|
||||
server.registerTool(
|
||||
"tool_name",
|
||||
{
|
||||
description: "工具描述",
|
||||
inputSchema: {
|
||||
param1: z.string().describe("参数1描述"),
|
||||
param2: z.number().describe("参数2描述"),
|
||||
},
|
||||
},
|
||||
async ({ param1, param2 }) => {
|
||||
// 工具逻辑
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "结果",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 开发模式
|
||||
|
||||
使用 watch 模式进行开发:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 相关资源
|
||||
|
||||
- [MCP 官方文档](https://modelcontextprotocol.io/)
|
||||
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
|
||||
- [MCP 服务器示例](https://github.com/modelcontextprotocol/servers)
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
BIN
golang/demo.db
Normal file
BIN
golang/demo.db
Normal file
Binary file not shown.
14
golang/go.mod
Normal file
14
golang/go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module mcp-demo-go
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/modelcontextprotocol/go-sdk v1.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/jsonschema-go v0.3.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
)
|
||||
16
golang/go.sum
Normal file
16
golang/go.sum
Normal file
@@ -0,0 +1,16 @@
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
||||
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
|
||||
github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
293
golang/main.go
Normal file
293
golang/main.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
// ==================== 数据库初始化 ====================
|
||||
|
||||
func initDB() error {
|
||||
// 获取可执行文件所在目录
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
execPath = "."
|
||||
}
|
||||
dbPath := filepath.Join(filepath.Dir(execPath), "demo.db")
|
||||
|
||||
// 如果数据库不存在,使用当前目录
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
dbPath = "demo.db"
|
||||
}
|
||||
|
||||
db, err = sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建示例表
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE,
|
||||
age INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
price REAL,
|
||||
stock INTEGER DEFAULT 0
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 插入示例数据(如果表为空)
|
||||
var count int
|
||||
db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
|
||||
if count == 0 {
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO users (name, email, age) VALUES
|
||||
('张三', 'zhangsan@example.com', 28),
|
||||
('李四', 'lisi@example.com', 32),
|
||||
('王五', 'wangwu@example.com', 25);
|
||||
|
||||
INSERT INTO products (name, price, stock) VALUES
|
||||
('iPhone 15', 6999.00, 100),
|
||||
('MacBook Pro', 14999.00, 50),
|
||||
('AirPods Pro', 1899.00, 200);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== 工具输入结构 ====================
|
||||
|
||||
// ListTablesInput - 列出所有表
|
||||
type ListTablesInput struct{}
|
||||
|
||||
// QueryInput - 执行 SQL 查询
|
||||
type QueryInput struct {
|
||||
SQL string `json:"sql" mcp:"SQL query to execute (SELECT only for safety)"`
|
||||
}
|
||||
|
||||
// GetUserInput - 获取用户
|
||||
type GetUserInput struct {
|
||||
ID int `json:"id" mcp:"User ID to retrieve"`
|
||||
}
|
||||
|
||||
// AddUserInput - 添加用户
|
||||
type AddUserInput struct {
|
||||
Name string `json:"name" mcp:"User name"`
|
||||
Email string `json:"email" mcp:"User email address"`
|
||||
Age int `json:"age" mcp:"User age"`
|
||||
}
|
||||
|
||||
// DeleteUserInput - 删除用户
|
||||
type DeleteUserInput struct {
|
||||
ID int `json:"id" mcp:"User ID to delete"`
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
func textResult(text string) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: text},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func errorResult(text string) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: text},
|
||||
},
|
||||
IsError: true,
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// ==================== 工具实现 ====================
|
||||
|
||||
// ListTables - 列出数据库中的所有表
|
||||
func ListTables(ctx context.Context, req *mcp.CallToolRequest, input ListTablesInput) (*mcp.CallToolResult, any, error) {
|
||||
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
if err != nil {
|
||||
return errorResult(fmt.Sprintf("Failed to list tables: %v", err))
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var name string
|
||||
rows.Scan(&name)
|
||||
tables = append(tables, name)
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(tables, "", " ")
|
||||
return textResult(fmt.Sprintf("Tables in database:\n%s", string(result)))
|
||||
}
|
||||
|
||||
// QueryDB - 执行 SQL 查询
|
||||
func QueryDB(ctx context.Context, req *mcp.CallToolRequest, input QueryInput) (*mcp.CallToolResult, any, error) {
|
||||
// 安全检查:只允许 SELECT 查询
|
||||
sqlUpper := strings.ToUpper(strings.TrimSpace(input.SQL))
|
||||
if !strings.HasPrefix(sqlUpper, "SELECT") {
|
||||
return errorResult("Only SELECT queries are allowed for safety")
|
||||
}
|
||||
|
||||
rows, err := db.Query(input.SQL)
|
||||
if err != nil {
|
||||
return errorResult(fmt.Sprintf("Query failed: %v", err))
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// 获取列名
|
||||
columns, _ := rows.Columns()
|
||||
|
||||
// 读取所有行
|
||||
var results []map[string]interface{}
|
||||
for rows.Next() {
|
||||
values := make([]interface{}, len(columns))
|
||||
valuePtrs := make([]interface{}, len(columns))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
rows.Scan(valuePtrs...)
|
||||
|
||||
row := make(map[string]interface{})
|
||||
for i, col := range columns {
|
||||
row[col] = values[i]
|
||||
}
|
||||
results = append(results, row)
|
||||
}
|
||||
|
||||
jsonResult, _ := json.MarshalIndent(results, "", " ")
|
||||
return textResult(fmt.Sprintf("Query results (%d rows):\n%s", len(results), string(jsonResult)))
|
||||
}
|
||||
|
||||
// GetUser - 获取单个用户
|
||||
func GetUser(ctx context.Context, req *mcp.CallToolRequest, input GetUserInput) (*mcp.CallToolResult, any, error) {
|
||||
var id int
|
||||
var name, email string
|
||||
var age int
|
||||
var createdAt string
|
||||
|
||||
err := db.QueryRow("SELECT id, name, email, age, created_at FROM users WHERE id = ?", input.ID).
|
||||
Scan(&id, &name, &email, &age, &createdAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return errorResult(fmt.Sprintf("User with ID %d not found", input.ID))
|
||||
}
|
||||
if err != nil {
|
||||
return errorResult(fmt.Sprintf("Query failed: %v", err))
|
||||
}
|
||||
|
||||
user := map[string]interface{}{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"email": email,
|
||||
"age": age,
|
||||
"created_at": createdAt,
|
||||
}
|
||||
|
||||
jsonResult, _ := json.MarshalIndent(user, "", " ")
|
||||
return textResult(string(jsonResult))
|
||||
}
|
||||
|
||||
// AddUser - 添加用户
|
||||
func AddUser(ctx context.Context, req *mcp.CallToolRequest, input AddUserInput) (*mcp.CallToolResult, any, error) {
|
||||
result, err := db.Exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
|
||||
input.Name, input.Email, input.Age)
|
||||
if err != nil {
|
||||
return errorResult(fmt.Sprintf("Failed to add user: %v", err))
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
return textResult(fmt.Sprintf("User added successfully with ID: %d", id))
|
||||
}
|
||||
|
||||
// DeleteUser - 删除用户
|
||||
func DeleteUser(ctx context.Context, req *mcp.CallToolRequest, input DeleteUserInput) (*mcp.CallToolResult, any, error) {
|
||||
result, err := db.Exec("DELETE FROM users WHERE id = ?", input.ID)
|
||||
if err != nil {
|
||||
return errorResult(fmt.Sprintf("Failed to delete user: %v", err))
|
||||
}
|
||||
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected == 0 {
|
||||
return errorResult(fmt.Sprintf("User with ID %d not found", input.ID))
|
||||
}
|
||||
|
||||
return textResult(fmt.Sprintf("User with ID %d deleted successfully", input.ID))
|
||||
}
|
||||
|
||||
// ==================== 主函数 ====================
|
||||
|
||||
func main() {
|
||||
// 初始化数据库
|
||||
if err := initDB(); err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 创建 MCP Server
|
||||
server := mcp.NewServer(
|
||||
&mcp.Implementation{
|
||||
Name: "mcp-demo-go",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
// 注册工具
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "list_tables",
|
||||
Description: "List all tables in the SQLite database",
|
||||
}, ListTables)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "query_db",
|
||||
Description: "Execute a SELECT SQL query on the database",
|
||||
}, QueryDB)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "get_user",
|
||||
Description: "Get a user by ID from the users table",
|
||||
}, GetUser)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "add_user",
|
||||
Description: "Add a new user to the database",
|
||||
}, AddUser)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "delete_user",
|
||||
Description: "Delete a user from the database by ID",
|
||||
}, DeleteUser)
|
||||
|
||||
// 运行服务器
|
||||
log.Println("MCP Demo Go Server (SQLite) is running...")
|
||||
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}
|
||||
1162
typescript/package-lock.json
generated
Normal file
1162
typescript/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
typescript/package.json
Normal file
32
typescript/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "mcp-demo-server",
|
||||
"version": "1.0.0",
|
||||
"description": "A demo MCP server with example tools",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-demo": "./build/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node build/index.js",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"ai",
|
||||
"tools"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
29
typescript/src/index.ts
Normal file
29
typescript/src/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* MCP Demo Server - TypeScript
|
||||
*
|
||||
* A demonstration MCP server with modular architecture:
|
||||
* - Tools: echo, calculator, time, random, string utilities
|
||||
* - Resources: skills capabilities, tool info
|
||||
* - Prompts: list-skills, process-text, math-helper
|
||||
*/
|
||||
|
||||
import { server } from "./server.js";
|
||||
import { registerAllTools } from "./tools/index.js";
|
||||
import { registerAllResources } from "./resources/index.js";
|
||||
import { registerAllPrompts } from "./prompts/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
|
||||
// Register all modules
|
||||
registerAllTools();
|
||||
registerAllResources();
|
||||
registerAllPrompts();
|
||||
|
||||
// Start the server
|
||||
async function main() {
|
||||
await server.connect(new StdioServerTransport());
|
||||
console.error("MCP Demo Server (TypeScript) running on stdio");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
15
typescript/src/prompts/index.ts
Normal file
15
typescript/src/prompts/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { registerListSkillsPrompt } from "./list-skills.js";
|
||||
import { registerProcessTextPrompt } from "./process-text.js";
|
||||
import { registerMathHelperPrompt } from "./math-helper.js";
|
||||
|
||||
export function registerAllPrompts() {
|
||||
registerListSkillsPrompt();
|
||||
registerProcessTextPrompt();
|
||||
registerMathHelperPrompt();
|
||||
}
|
||||
|
||||
export {
|
||||
registerListSkillsPrompt,
|
||||
registerProcessTextPrompt,
|
||||
registerMathHelperPrompt,
|
||||
};
|
||||
74
typescript/src/prompts/list-skills.ts
Normal file
74
typescript/src/prompts/list-skills.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { server } from "../server.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerListSkillsPrompt() {
|
||||
server.registerPrompt(
|
||||
"list-skills",
|
||||
{
|
||||
description:
|
||||
"List all available skills and capabilities of this MCP server",
|
||||
argsSchema: {
|
||||
format: z
|
||||
.enum(["brief", "detailed"])
|
||||
.optional()
|
||||
.describe("Output format: 'brief' or 'detailed'"),
|
||||
},
|
||||
},
|
||||
async ({ format }) => {
|
||||
const isDetailed = format === "detailed";
|
||||
|
||||
const briefContent = `This MCP server provides 5 tools:
|
||||
1. echo - Echo messages
|
||||
2. calculator - Math operations (+, -, *, /)
|
||||
3. get_current_time - Current time with timezone support
|
||||
4. random_number - Random number generation
|
||||
5. string_utils - String manipulation`;
|
||||
|
||||
const detailedContent = `# MCP Demo Server - Complete Skills Guide
|
||||
|
||||
## Tools Overview
|
||||
|
||||
### 1. echo
|
||||
Echo back any message you send.
|
||||
\`\`\`json
|
||||
{"message": "Hello!"}
|
||||
\`\`\`
|
||||
|
||||
### 2. calculator
|
||||
Perform basic math: add, subtract, multiply, divide.
|
||||
\`\`\`json
|
||||
{"operation": "add", "a": 10, "b": 5}
|
||||
\`\`\`
|
||||
|
||||
### 3. get_current_time
|
||||
Get current time, optionally in a specific timezone.
|
||||
\`\`\`json
|
||||
{"timezone": "Asia/Shanghai"}
|
||||
\`\`\`
|
||||
|
||||
### 4. random_number
|
||||
Generate random numbers in a range.
|
||||
\`\`\`json
|
||||
{"min": 1, "max": 100}
|
||||
\`\`\`
|
||||
|
||||
### 5. string_utils
|
||||
String operations: uppercase, lowercase, reverse, length, word_count.
|
||||
\`\`\`json
|
||||
{"operation": "uppercase", "text": "hello world"}
|
||||
\`\`\``;
|
||||
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "text",
|
||||
text: isDetailed ? detailedContent : briefContent,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
35
typescript/src/prompts/math-helper.ts
Normal file
35
typescript/src/prompts/math-helper.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { server } from "../server.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerMathHelperPrompt() {
|
||||
server.registerPrompt(
|
||||
"math-helper",
|
||||
{
|
||||
description: "Generate a prompt for mathematical calculations",
|
||||
argsSchema: {
|
||||
problem: z.string().describe("The math problem to solve"),
|
||||
},
|
||||
},
|
||||
async ({ problem }) => {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `Please help solve this math problem: ${problem}
|
||||
|
||||
Use the calculator tool to perform the calculations. Show your work step by step.
|
||||
|
||||
Available operations:
|
||||
- add (a + b)
|
||||
- subtract (a - b)
|
||||
- multiply (a * b)
|
||||
- divide (a / b)`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
59
typescript/src/prompts/process-text.ts
Normal file
59
typescript/src/prompts/process-text.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { server } from "../server.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerProcessTextPrompt() {
|
||||
server.registerPrompt(
|
||||
"process-text",
|
||||
{
|
||||
description: "Generate a prompt for text processing tasks",
|
||||
argsSchema: {
|
||||
text: z.string().describe("The text to process"),
|
||||
task: z
|
||||
.enum(["analyze", "transform", "summarize"])
|
||||
.optional()
|
||||
.describe("Processing task: 'analyze', 'transform', or 'summarize'"),
|
||||
},
|
||||
},
|
||||
async ({ text, task }) => {
|
||||
const taskType = task || "analyze";
|
||||
|
||||
const prompts: Record<string, string> = {
|
||||
analyze: `Please analyze the following text and provide insights:
|
||||
|
||||
Text: "${text}"
|
||||
|
||||
Use the string_utils tool to:
|
||||
1. Get the length of the text
|
||||
2. Count the words
|
||||
3. Show the text in uppercase and lowercase`,
|
||||
|
||||
transform: `Please transform the following text:
|
||||
|
||||
Text: "${text}"
|
||||
|
||||
Use the string_utils tool to:
|
||||
1. Convert to UPPERCASE
|
||||
2. Convert to lowercase
|
||||
3. Reverse the text`,
|
||||
|
||||
summarize: `Please provide a summary of the following text:
|
||||
|
||||
Text: "${text}"
|
||||
|
||||
First, use string_utils to get basic stats (length, word_count), then provide your analysis.`,
|
||||
};
|
||||
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "text",
|
||||
text: prompts[taskType] || prompts["analyze"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
9
typescript/src/resources/index.ts
Normal file
9
typescript/src/resources/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { registerSkillsResource } from "./skills.js";
|
||||
import { registerToolInfoResource, AVAILABLE_TOOLS } from "./tool-info.js";
|
||||
|
||||
export function registerAllResources() {
|
||||
registerSkillsResource();
|
||||
registerToolInfoResource();
|
||||
}
|
||||
|
||||
export { registerSkillsResource, registerToolInfoResource, AVAILABLE_TOOLS };
|
||||
56
typescript/src/resources/skills.ts
Normal file
56
typescript/src/resources/skills.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { server } from "../server.js";
|
||||
|
||||
const SKILLS_CONTENT = `# MCP Demo Server Skills
|
||||
|
||||
## Available Tools
|
||||
|
||||
### 1. echo
|
||||
- **Description**: Echo back the provided message
|
||||
- **Use Case**: Testing connectivity, message relay
|
||||
|
||||
### 2. calculator
|
||||
- **Description**: Perform basic mathematical operations (add, subtract, multiply, divide)
|
||||
- **Use Case**: Mathematical calculations
|
||||
|
||||
### 3. get_current_time
|
||||
- **Description**: Get the current date and time with optional timezone
|
||||
- **Use Case**: Time queries, timezone conversions
|
||||
|
||||
### 4. random_number
|
||||
- **Description**: Generate a random number within a specified range
|
||||
- **Use Case**: Random selection, gaming, testing
|
||||
|
||||
### 5. string_utils
|
||||
- **Description**: Perform string operations (uppercase, lowercase, reverse, length, word_count)
|
||||
- **Use Case**: Text processing, string manipulation
|
||||
|
||||
## Capabilities Summary
|
||||
- Basic I/O operations
|
||||
- Mathematical calculations
|
||||
- Time and date handling
|
||||
- Random number generation
|
||||
- String manipulation
|
||||
|
||||
## How to Use
|
||||
Call any tool with its required parameters. Use the \`list-skills\` prompt for interactive guidance.
|
||||
`;
|
||||
|
||||
export function registerSkillsResource() {
|
||||
server.registerResource(
|
||||
"skills-capabilities",
|
||||
"mcp-demo://skills/capabilities",
|
||||
{
|
||||
description: "Complete list of server capabilities and skills",
|
||||
mimeType: "text/markdown",
|
||||
},
|
||||
async () => ({
|
||||
contents: [
|
||||
{
|
||||
uri: "mcp-demo://skills/capabilities",
|
||||
mimeType: "text/markdown",
|
||||
text: SKILLS_CONTENT,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
123
typescript/src/resources/tool-info.ts
Normal file
123
typescript/src/resources/tool-info.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { server } from "../server.js";
|
||||
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
interface ToolMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, string>;
|
||||
examples: string[];
|
||||
}
|
||||
|
||||
const TOOL_METADATA: Record<string, ToolMetadata> = {
|
||||
echo: {
|
||||
name: "echo",
|
||||
description: "Echo back the provided message",
|
||||
parameters: {
|
||||
message: "string - The message to echo back",
|
||||
},
|
||||
examples: ['{"message": "Hello, World!"}'],
|
||||
},
|
||||
calculator: {
|
||||
name: "calculator",
|
||||
description: "Perform basic mathematical operations",
|
||||
parameters: {
|
||||
operation: "enum - add, subtract, multiply, divide",
|
||||
a: "number - First operand",
|
||||
b: "number - Second operand",
|
||||
},
|
||||
examples: [
|
||||
'{"operation": "add", "a": 10, "b": 5}',
|
||||
'{"operation": "multiply", "a": 3, "b": 7}',
|
||||
],
|
||||
},
|
||||
get_current_time: {
|
||||
name: "get_current_time",
|
||||
description: "Get the current date and time",
|
||||
parameters: {
|
||||
timezone: "string (optional) - Timezone like 'Asia/Shanghai'",
|
||||
},
|
||||
examples: ["{}", '{"timezone": "America/New_York"}'],
|
||||
},
|
||||
random_number: {
|
||||
name: "random_number",
|
||||
description: "Generate a random number within a range",
|
||||
parameters: {
|
||||
min: "number - Minimum value (inclusive)",
|
||||
max: "number - Maximum value (inclusive)",
|
||||
},
|
||||
examples: ['{"min": 1, "max": 100}'],
|
||||
},
|
||||
string_utils: {
|
||||
name: "string_utils",
|
||||
description: "Perform string operations",
|
||||
parameters: {
|
||||
operation: "enum - uppercase, lowercase, reverse, length, word_count",
|
||||
text: "string - The text to process",
|
||||
},
|
||||
examples: [
|
||||
'{"operation": "uppercase", "text": "hello"}',
|
||||
'{"operation": "word_count", "text": "Hello World"}',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const AVAILABLE_TOOLS = Object.keys(TOOL_METADATA);
|
||||
|
||||
export function registerToolInfoResource() {
|
||||
server.registerResource(
|
||||
"tool-info",
|
||||
new ResourceTemplate("mcp-demo://tools/{toolName}", {
|
||||
list: async () => ({
|
||||
resources: AVAILABLE_TOOLS.map((name) => ({
|
||||
uri: `mcp-demo://tools/${name}`,
|
||||
name: `Tool: ${name}`,
|
||||
description: TOOL_METADATA[name].description,
|
||||
mimeType: "application/json",
|
||||
})),
|
||||
}),
|
||||
complete: {
|
||||
toolName: async (value: string) =>
|
||||
AVAILABLE_TOOLS.filter((name) =>
|
||||
name.toLowerCase().startsWith(value.toLowerCase())
|
||||
),
|
||||
},
|
||||
}),
|
||||
{
|
||||
description: "Detailed information about a specific tool",
|
||||
mimeType: "application/json",
|
||||
},
|
||||
async (uri: URL) => {
|
||||
const toolName = uri.pathname.split("/").pop() || "";
|
||||
const metadata = TOOL_METADATA[toolName];
|
||||
|
||||
if (!metadata) {
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
error: `Tool '${toolName}' not found`,
|
||||
availableTools: AVAILABLE_TOOLS,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(metadata, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
7
typescript/src/server.ts
Normal file
7
typescript/src/server.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
// 创建 MCP Server 实例
|
||||
export const server = new McpServer({
|
||||
name: "mcp-demo-server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
56
typescript/src/tools/calculator.ts
Normal file
56
typescript/src/tools/calculator.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { server } from "../server.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerCalculatorTool() {
|
||||
server.registerTool(
|
||||
"calculator",
|
||||
{
|
||||
description: "Perform basic mathematical operations",
|
||||
inputSchema: {
|
||||
operation: z
|
||||
.enum(["add", "subtract", "multiply", "divide"])
|
||||
.describe("The operation to perform"),
|
||||
a: z.number().describe("First operand"),
|
||||
b: z.number().describe("Second operand"),
|
||||
},
|
||||
},
|
||||
async ({ operation, a, b }) => {
|
||||
let result: number;
|
||||
|
||||
switch (operation) {
|
||||
case "add":
|
||||
result = a + b;
|
||||
break;
|
||||
case "subtract":
|
||||
result = a - b;
|
||||
break;
|
||||
case "multiply":
|
||||
result = a * b;
|
||||
break;
|
||||
case "divide":
|
||||
if (b === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Division by zero is not allowed",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
result = a / b;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${a} ${operation} ${b} = ${result}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
24
typescript/src/tools/echo.ts
Normal file
24
typescript/src/tools/echo.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { server } from "../server.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerEchoTool() {
|
||||
server.registerTool(
|
||||
"echo",
|
||||
{
|
||||
description: "Echo back the provided message",
|
||||
inputSchema: {
|
||||
message: z.string().describe("The message to echo back"),
|
||||
},
|
||||
},
|
||||
async ({ message }) => {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Echo: ${message}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
21
typescript/src/tools/index.ts
Normal file
21
typescript/src/tools/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { registerEchoTool } from "./echo.js";
|
||||
import { registerCalculatorTool } from "./calculator.js";
|
||||
import { registerTimeTool } from "./time.js";
|
||||
import { registerRandomTool } from "./random.js";
|
||||
import { registerStringTool } from "./string.js";
|
||||
|
||||
export function registerAllTools() {
|
||||
registerEchoTool();
|
||||
registerCalculatorTool();
|
||||
registerTimeTool();
|
||||
registerRandomTool();
|
||||
registerStringTool();
|
||||
}
|
||||
|
||||
export {
|
||||
registerEchoTool,
|
||||
registerCalculatorTool,
|
||||
registerTimeTool,
|
||||
registerRandomTool,
|
||||
registerStringTool,
|
||||
};
|
||||
39
typescript/src/tools/random.ts
Normal file
39
typescript/src/tools/random.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { server } from "../server.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerRandomTool() {
|
||||
server.registerTool(
|
||||
"random_number",
|
||||
{
|
||||
description: "Generate a random number within a specified range",
|
||||
inputSchema: {
|
||||
min: z.number().describe("Minimum value (inclusive)"),
|
||||
max: z.number().describe("Maximum value (inclusive)"),
|
||||
},
|
||||
},
|
||||
async ({ min, max }) => {
|
||||
if (min > max) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: min must be less than or equal to max",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Random number between ${min} and ${max}: ${randomNum}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
51
typescript/src/tools/string.ts
Normal file
51
typescript/src/tools/string.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { server } from "../server.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerStringTool() {
|
||||
server.registerTool(
|
||||
"string_utils",
|
||||
{
|
||||
description: "Perform various string operations",
|
||||
inputSchema: {
|
||||
operation: z
|
||||
.enum(["uppercase", "lowercase", "reverse", "length", "word_count"])
|
||||
.describe("The string operation to perform"),
|
||||
text: z.string().describe("The text to process"),
|
||||
},
|
||||
},
|
||||
async ({ operation, text }) => {
|
||||
let result: string;
|
||||
|
||||
switch (operation) {
|
||||
case "uppercase":
|
||||
result = text.toUpperCase();
|
||||
break;
|
||||
case "lowercase":
|
||||
result = text.toLowerCase();
|
||||
break;
|
||||
case "reverse":
|
||||
result = text.split("").reverse().join("");
|
||||
break;
|
||||
case "length":
|
||||
result = `Length: ${text.length} characters`;
|
||||
break;
|
||||
case "word_count":
|
||||
const words = text
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((w) => w.length > 0);
|
||||
result = `Word count: ${words.length}`;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
48
typescript/src/tools/time.ts
Normal file
48
typescript/src/tools/time.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { server } from "../server.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerTimeTool() {
|
||||
server.registerTool(
|
||||
"get_current_time",
|
||||
{
|
||||
description: "Get the current date and time",
|
||||
inputSchema: {
|
||||
timezone: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Timezone (e.g., 'Asia/Shanghai', 'America/New_York')"),
|
||||
},
|
||||
},
|
||||
async ({ timezone }) => {
|
||||
const now = new Date();
|
||||
let timeString: string;
|
||||
|
||||
if (timezone) {
|
||||
try {
|
||||
timeString = now.toLocaleString("en-US", { timeZone: timezone });
|
||||
} catch {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Invalid timezone '${timezone}'`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
timeString = now.toISOString();
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Current time: ${timeString}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
23
typescript/tsconfig.json
Normal file
23
typescript/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user