qdy 1 месяц назад
Сommit
be1aa3f86e

+ 1
- 0
.gitignore Просмотреть файл

@@ -0,0 +1 @@
1
+logs/

+ 25
- 0
gct.sh Просмотреть файл

@@ -0,0 +1,25 @@
1
+#!/bin/bash
2
+
3
+# 使用命令行参数,如果没有提供则使用默认值
4
+COMMIT_MSG="${1:-初始提交}"
5
+TAG_VERSION="${2:-v0.1.0}"
6
+
7
+echo "提交信息: $COMMIT_MSG"
8
+echo "标签版本: $TAG_VERSION"
9
+
10
+# 提交和推送
11
+git add .
12
+git commit -m "$COMMIT_MSG"
13
+git push -u origin master
14
+
15
+# 创建标签(关键修改在这里)
16
+# 方法1:使用注释标签(推荐,不会打开编辑器)
17
+git tag -a "$TAG_VERSION" -m "Release $TAG_VERSION"
18
+
19
+# 或者方法2:如果你需要GPG签名,使用这个
20
+# GIT_EDITOR=true git tag -s "$TAG_VERSION" -m "Release $TAG_VERSION"
21
+
22
+# 推送标签
23
+git push origin "$TAG_VERSION"
24
+
25
+echo "完成!"

+ 34
- 0
go.mod Просмотреть файл

@@ -0,0 +1,34 @@
1
+module git.x2erp.com/qdy/go-svc-mcp
2
+
3
+go 1.25.4
4
+
5
+replace git.x2erp.com/qdy/go-base => ../go-base
6
+
7
+replace git.x2erp.com/qdy/go-db => ../go-db
8
+
9
+require (
10
+	git.x2erp.com/qdy/go-base v0.1.15
11
+	git.x2erp.com/qdy/go-db v0.0.0-00010101000000-000000000000
12
+	github.com/modelcontextprotocol/go-sdk v1.2.0
13
+)
14
+
15
+require (
16
+	filippo.io/edwards25519 v1.1.0 // indirect
17
+	github.com/go-sql-driver/mysql v1.9.3 // indirect
18
+	github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
19
+	github.com/golang-sql/sqlexp v0.1.0 // indirect
20
+	github.com/google/jsonschema-go v0.3.0 // indirect
21
+	github.com/google/uuid v1.6.0 // indirect
22
+	github.com/jmoiron/sqlx v1.4.0 // indirect
23
+	github.com/lib/pq v1.10.9 // indirect
24
+	github.com/microsoft/go-mssqldb v1.9.4 // indirect
25
+	github.com/sijms/go-ora/v2 v2.9.0 // indirect
26
+	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
27
+	go.uber.org/multierr v1.10.0 // indirect
28
+	go.uber.org/zap v1.27.1 // indirect
29
+	golang.org/x/crypto v0.46.0 // indirect
30
+	golang.org/x/oauth2 v0.30.0 // indirect
31
+	golang.org/x/text v0.32.0 // indirect
32
+	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
33
+	gopkg.in/yaml.v2 v2.4.0 // indirect
34
+)

+ 86
- 0
go.sum Просмотреть файл

@@ -0,0 +1,86 @@
1
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
4
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
5
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
6
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
7
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
8
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
9
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
10
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
11
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
12
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
13
+github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
14
+github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
15
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
16
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
18
+github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
19
+github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
20
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
21
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
22
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
23
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
24
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
25
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
26
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
27
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
28
+github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
29
+github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
30
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
31
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
32
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
33
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
34
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
35
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
36
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
37
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
38
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
39
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
40
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
41
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
42
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
43
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
44
+github.com/microsoft/go-mssqldb v1.9.4 h1:sHrj3GcdgkxytZ09aZ3+ys72pMeyEXJowT44j74pNgs=
45
+github.com/microsoft/go-mssqldb v1.9.4/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
46
+github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
47
+github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
48
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
49
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
50
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
51
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
52
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
53
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
54
+github.com/sijms/go-ora/v2 v2.9.0 h1:+iQbUeTeCOFMb5BsOMgUhV8KWyrv9yjKpcK4x7+MFrg=
55
+github.com/sijms/go-ora/v2 v2.9.0/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU=
56
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
57
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
58
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
59
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
60
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
61
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
62
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
63
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
64
+go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
65
+go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
66
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
67
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
68
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
69
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
70
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
71
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
72
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
73
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
74
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
75
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
76
+golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
77
+golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
78
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
79
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
80
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
81
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
82
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
83
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
84
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
85
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
86
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 31
- 0
internal/auth/auth_middleware.go Просмотреть файл

@@ -0,0 +1,31 @@
1
+package auth
2
+
3
+import (
4
+	"context"
5
+	"net/http"
6
+)
7
+
8
+// authMiddleware 验证 Authorization 头和项目 ID 头
9
+func AuthMiddleware(next http.Handler, authToken, projectIDHeader string) http.Handler {
10
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
11
+		// 验证 Authorization 头
12
+		authHeader := r.Header.Get("Authorization")
13
+		if authHeader == "" {
14
+			http.Error(w, "Authorization header required", http.StatusUnauthorized)
15
+			return
16
+		}
17
+		expected := "Bearer " + authToken
18
+		if authHeader != expected {
19
+			http.Error(w, "Invalid authorization token", http.StatusUnauthorized)
20
+			return
21
+		}
22
+		// 提取项目 ID 头并存储到请求上下文中,供 extractRequestContext 使用
23
+		projectID := r.Header.Get(projectIDHeader)
24
+		if projectID != "" {
25
+			// 将项目 ID 存储到上下文中
26
+			ctx := context.WithValue(r.Context(), "projectID", projectID)
27
+			r = r.WithContext(ctx)
28
+		}
29
+		next.ServeHTTP(w, r)
30
+	})
31
+}

+ 111
- 0
internal/mcp/registry.go Просмотреть файл

@@ -0,0 +1,111 @@
1
+package mcp
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"reflect"
7
+	"sync"
8
+
9
+	"git.x2erp.com/qdy/go-base/ctx"
10
+	"git.x2erp.com/qdy/go-db/factory/database"
11
+)
12
+
13
+// Tool 接口,工具实现此接口以支持自动注册
14
+type Tool interface {
15
+	Name() string
16
+	Description() string
17
+	InputSchema() map[string]interface{}
18
+	Execute(input json.RawMessage, dep *ToolDependencies) (interface{}, error)
19
+}
20
+
21
+// ToolDefinition 定义 MCP 工具
22
+type ToolDefinition struct {
23
+	Name        string                 `json:"name"`
24
+	Description string                 `json:"description"`
25
+	InputSchema map[string]interface{} `json:"inputSchema"`
26
+	Execute     ToolExecuteFunc        `json:"-"`
27
+}
28
+
29
+// ToolExecuteFunc 工具执行函数签名
30
+type ToolExecuteFunc func(input json.RawMessage, dep *ToolDependencies) (interface{}, error)
31
+
32
+// ToolDependencies 工具执行依赖项
33
+type ToolDependencies struct {
34
+	DBFactory *database.DBFactory
35
+	ReqCtx    *ctx.RequestContext
36
+}
37
+
38
+// globalRegistry 全局工具注册表
39
+var (
40
+	globalRegistry   = make(map[string]ToolDefinition)
41
+	registryMu       sync.RWMutex
42
+	dependencies     *ToolDependencies
43
+	dependenciesOnce sync.Once
44
+)
45
+
46
+// Register 注册一个工具
47
+func Register(name, description string, inputSchema map[string]interface{}, execute ToolExecuteFunc) {
48
+	registryMu.Lock()
49
+	defer registryMu.Unlock()
50
+
51
+	if _, exists := globalRegistry[name]; exists {
52
+		panic(fmt.Sprintf("tool already registered: %s", name))
53
+	}
54
+
55
+	globalRegistry[name] = ToolDefinition{
56
+		Name:        name,
57
+		Description: description,
58
+		InputSchema: inputSchema,
59
+		Execute:     execute,
60
+	}
61
+}
62
+
63
+// GetTool 获取工具定义
64
+func GetTool(name string) (ToolDefinition, bool) {
65
+	registryMu.RLock()
66
+	defer registryMu.RUnlock()
67
+	tool, ok := globalRegistry[name]
68
+	return tool, ok
69
+}
70
+
71
+// ListTools 返回所有工具定义
72
+func ListTools() []ToolDefinition {
73
+	registryMu.RLock()
74
+	defer registryMu.RUnlock()
75
+	tools := make([]ToolDefinition, 0, len(globalRegistry))
76
+	for _, tool := range globalRegistry {
77
+		tools = append(tools, tool)
78
+	}
79
+	return tools
80
+}
81
+
82
+// SetDependencies 设置全局依赖项
83
+func SetDependencies(dbFactory *database.DBFactory, reqCtx *ctx.RequestContext) {
84
+	dependenciesOnce.Do(func() {
85
+		dependencies = &ToolDependencies{
86
+			DBFactory: dbFactory,
87
+			ReqCtx:    reqCtx,
88
+		}
89
+	})
90
+}
91
+
92
+// GetDependencies 获取依赖项(如果已设置)
93
+func GetDependencies() *ToolDependencies {
94
+	return dependencies
95
+}
96
+
97
+// AutoRegister 自动注册实现 Tool 接口的类型
98
+func AutoRegister(tool interface{}) {
99
+	val := reflect.ValueOf(tool)
100
+	typ := val.Type()
101
+
102
+	// 检查是否实现了 Tool 接口
103
+	if tool, ok := tool.(Tool); ok {
104
+		Register(tool.Name(), tool.Description(), tool.InputSchema(), tool.Execute)
105
+		return
106
+	}
107
+
108
+	// 检查是否具有适当方法的其他接口
109
+	// 这里可以根据需要扩展
110
+	panic(fmt.Sprintf("type %v does not implement Tool interface", typ))
111
+}

+ 186
- 0
internal/mcp/server.go Просмотреть файл

@@ -0,0 +1,186 @@
1
+package mcp
2
+
3
+import (
4
+	"context"
5
+	"encoding/json"
6
+	"fmt"
7
+	"log"
8
+	"net/http"
9
+	"os"
10
+
11
+	"git.x2erp.com/qdy/go-base/ctx"
12
+	"git.x2erp.com/qdy/go-db/factory/database"
13
+	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
14
+)
15
+
16
+// Server 包装 MCP SDK 服务器,提供自动注册和依赖注入
17
+type Server struct {
18
+	port        int
19
+	serviceName string
20
+	sdkServer   *mcpsdk.Server
21
+	transport   mcpsdk.Transport
22
+	dbFactory   *database.DBFactory
23
+	baseCtx     *ctx.RequestContext
24
+	httpServer  *http.Server
25
+	handler     http.Handler
26
+}
27
+
28
+// Config 服务器配置
29
+type Config struct {
30
+	Name        string
31
+	Version     string
32
+	Port        int
33
+	ServiceName string
34
+	Description string
35
+	DBFactory   *database.DBFactory
36
+	BaseCtx     *ctx.RequestContext
37
+}
38
+
39
+// NewServer 创建新的 MCP 服务器
40
+func NewServer(cfg Config) (*Server, error) {
41
+	impl := &mcpsdk.Implementation{
42
+		Name:    cfg.Name,
43
+		Version: cfg.Version,
44
+	}
45
+	sdkServer := mcpsdk.NewServer(impl, nil)
46
+
47
+	server := &Server{
48
+		sdkServer:   sdkServer,
49
+		dbFactory:   cfg.DBFactory,
50
+		baseCtx:     cfg.BaseCtx,
51
+		port:        cfg.Port,
52
+		serviceName: cfg.ServiceName,
53
+	}
54
+
55
+	// 自动注册所有已注册的工具
56
+	if err := server.registerAllTools(); err != nil {
57
+		return nil, fmt.Errorf("failed to register tools: %w", err)
58
+	}
59
+
60
+	return server, nil
61
+}
62
+
63
+// registerAllTools 将注册表中的所有工具注册到 MCP 服务器
64
+func (s *Server) registerAllTools() error {
65
+	tools := ListTools()
66
+	for _, tool := range tools {
67
+		if err := s.registerTool(tool); err != nil {
68
+			return fmt.Errorf("failed to register tool %s: %w", tool.Name, err)
69
+		}
70
+	}
71
+	log.Printf("Registered %d MCP tools", len(tools))
72
+	return nil
73
+}
74
+
75
+// registerTool 注册单个工具到 MCP 服务器
76
+func (s *Server) registerTool(tool ToolDefinition) error {
77
+	// 创建工具处理器
78
+	handler := s.createToolHandler(tool)
79
+
80
+	// 创建 MCP 工具
81
+	mcpTool := &mcpsdk.Tool{
82
+		Name:        tool.Name,
83
+		Description: tool.Description,
84
+		InputSchema: tool.InputSchema,
85
+	}
86
+
87
+	// 注册工具到服务器
88
+	mcpsdk.AddTool(s.sdkServer, mcpTool, handler)
89
+	return nil
90
+}
91
+
92
+// createToolHandler 创建 MCP 工具处理器
93
+func (s *Server) createToolHandler(tool ToolDefinition) mcpsdk.ToolHandlerFor[map[string]interface{}, interface{}] {
94
+	return func(_ context.Context, request *mcpsdk.CallToolRequest, input map[string]interface{}) (*mcpsdk.CallToolResult, interface{}, error) {
95
+		// 将输入转换为 JSON
96
+		inputJSON, err := json.Marshal(input)
97
+		if err != nil {
98
+			return nil, nil, fmt.Errorf("failed to marshal input: %w", err)
99
+		}
100
+
101
+		// 提取请求上下文信息
102
+		reqCtx := s.extractRequestContext(request)
103
+
104
+		// 创建工具依赖项
105
+		toolDeps := &ToolDependencies{
106
+			DBFactory: s.dbFactory,
107
+			ReqCtx:    reqCtx,
108
+		}
109
+
110
+		// 执行工具
111
+		result, err := tool.Execute(json.RawMessage(inputJSON), toolDeps)
112
+		if err != nil {
113
+			// 返回工具错误(非协议错误)
114
+			return &mcpsdk.CallToolResult{
115
+				IsError: true,
116
+				Content: []mcpsdk.Content{
117
+					&mcpsdk.TextContent{Text: fmt.Sprintf("tool error: %v", err)},
118
+				},
119
+			}, nil, nil
120
+		}
121
+
122
+		// 返回成功结果
123
+		return nil, result, nil
124
+	}
125
+}
126
+
127
+// extractRequestContext 从 MCP 请求中提取上下文信息
128
+func (s *Server) extractRequestContext(request *mcpsdk.CallToolRequest) *ctx.RequestContext {
129
+	reqCtx := &ctx.RequestContext{}
130
+	if s.baseCtx != nil {
131
+		// 复制基础上下文
132
+		*reqCtx = *s.baseCtx
133
+	}
134
+
135
+	// 从请求的 Extra 数据中提取自定义项目 ID
136
+	extra := request.GetExtra()
137
+	if extra != nil && extra.Header != nil {
138
+		// 确定项目 ID 头名称
139
+		projectIDHeader := os.Getenv("MCP_PROJECT_ID_HEADER")
140
+		if projectIDHeader == "" {
141
+			projectIDHeader = "X-Project-ID"
142
+		}
143
+		if projectID := extra.Header.Get(projectIDHeader); projectID != "" {
144
+			// 将项目 ID 存储在 TraceID 中
145
+			reqCtx.ProjectID = projectID
146
+		}
147
+	}
148
+
149
+	return reqCtx
150
+}
151
+
152
+// SetTransport 设置传输层
153
+func (s *Server) SetTransport(transport mcpsdk.Transport) {
154
+	s.transport = transport
155
+}
156
+
157
+// GetSDKServer 返回底层的 SDK 服务器实例
158
+func (s *Server) GetSDKServer() *mcpsdk.Server {
159
+	return s.sdkServer
160
+}
161
+
162
+// startHTTPServer 启动 HTTP 服务器
163
+func (s *Server) Run(handler http.Handler) {
164
+
165
+	s.handler = handler
166
+	addr := fmt.Sprintf(":%d", s.port)
167
+
168
+	s.httpServer = &http.Server{
169
+		Addr:    addr,
170
+		Handler: s.handler,
171
+	}
172
+
173
+	log.Printf("%s listening on %s", s.serviceName, addr)
174
+
175
+	// 在 goroutine 中启动服务器
176
+	go func() {
177
+		if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
178
+			log.Fatalf("%s failed to start: %v", s.serviceName, err)
179
+		}
180
+	}()
181
+}
182
+
183
+// GetHTTPServer 返回内部的 HTTP 服务器实例
184
+func (s *Server) GetHTTPServer() *http.Server {
185
+	return s.httpServer
186
+}

+ 67
- 0
internal/tools/echo.go Просмотреть файл

@@ -0,0 +1,67 @@
1
+package tools
2
+
3
+import (
4
+	"encoding/json"
5
+
6
+	"git.x2erp.com/qdy/go-svc-mcp/internal/mcp"
7
+)
8
+
9
+func init() {
10
+	mcp.Register("echo", "回显输入的消息,演示自动注册和依赖注入",
11
+		map[string]interface{}{
12
+			"type": "object",
13
+			"properties": map[string]interface{}{
14
+				"message": map[string]interface{}{
15
+					"type":        "string",
16
+					"description": "要回显的消息",
17
+				},
18
+				"repeat": map[string]interface{}{
19
+					"type":        "integer",
20
+					"description": "重复次数",
21
+					"default":     1,
22
+					"minimum":     1,
23
+					"maximum":     10,
24
+				},
25
+			},
26
+			"required": []string{"message"},
27
+		},
28
+		func(input json.RawMessage, deps *mcp.ToolDependencies) (interface{}, error) {
29
+			var params struct {
30
+				Message string `json:"message"`
31
+				Repeat  int    `json:"repeat"`
32
+			}
33
+			if len(input) > 0 {
34
+				if err := json.Unmarshal(input, &params); err != nil {
35
+					return nil, err
36
+				}
37
+			}
38
+
39
+			if params.Repeat == 0 {
40
+				params.Repeat = 1
41
+			}
42
+
43
+			// 使用依赖项(数据库工厂和上下文)记录日志或执行其他操作
44
+			if deps.DBFactory != nil {
45
+				// 可以记录到数据库或执行查询
46
+				// 示例:记录工具调用
47
+			}
48
+
49
+			// 构建回显结果
50
+			result := ""
51
+			for i := 0; i < params.Repeat; i++ {
52
+				if i > 0 {
53
+					result += " "
54
+				}
55
+				result += params.Message
56
+			}
57
+
58
+			return map[string]interface{}{
59
+				"tenant_id": deps.ReqCtx.TenantID,
60
+				"original":  params.Message,
61
+				"echo":      result,
62
+				"repeat":    params.Repeat,
63
+				"has_db":    deps.DBFactory != nil,
64
+			}, nil
65
+		},
66
+	)
67
+}

+ 32
- 0
internal/tools/get_current_date.go Просмотреть файл

@@ -0,0 +1,32 @@
1
+package tools
2
+
3
+import (
4
+	"encoding/json"
5
+	"log"
6
+	"time"
7
+
8
+	"git.x2erp.com/qdy/go-svc-mcp/internal/mcp"
9
+)
10
+
11
+func init() {
12
+	mcp.Register("get_current_date", "获取当前服务器日期。所有以日期有关的计算,必须按此时间为基础计算。比如上年,最近7天等",
13
+		map[string]interface{}{
14
+			"type":       "object",
15
+			"properties": map[string]interface{}{},
16
+			"required":   []string{},
17
+		},
18
+		func(input json.RawMessage, deps *mcp.ToolDependencies) (interface{}, error) {
19
+			now := time.Now()
20
+			log.Printf("AI 调用工具-get_current_date: %s", now)
21
+			log.Printf("AI 项目ID: %s", deps.ReqCtx.ProjectID)
22
+			return map[string]interface{}{
23
+				"date":      now.Format("2006-01-02"),
24
+				"year":      now.Year(),
25
+				"month":     int(now.Month()),
26
+				"day":       now.Day(),
27
+				"iso_date":  now.Format(time.RFC3339),
28
+				"timestamp": now.Unix(),
29
+			}, nil
30
+		},
31
+	)
32
+}

+ 115
- 0
internal/tools/get_date_range.go Просмотреть файл

@@ -0,0 +1,115 @@
1
+package tools
2
+
3
+import (
4
+	"encoding/json"
5
+	"log"
6
+	"time"
7
+
8
+	"git.x2erp.com/qdy/go-svc-mcp/internal/mcp"
9
+)
10
+
11
+func init() {
12
+	mcp.Register("get_date_range", "根据时间单位和偏移量计算日期范围",
13
+		map[string]interface{}{
14
+			"type": "object",
15
+			"properties": map[string]interface{}{
16
+				"unit": map[string]interface{}{
17
+					"type":        "string",
18
+					"description": "时间单位",
19
+					"enum":        []string{"day", "week", "month", "quarter", "year"},
20
+				},
21
+				"offset": map[string]interface{}{
22
+					"type":        "integer",
23
+					"description": "偏移量(负数表示过去,正数表示未来)",
24
+					"default":     0,
25
+				},
26
+			},
27
+			"required": []string{"unit", "offset"},
28
+		},
29
+		func(input json.RawMessage, deps *mcp.ToolDependencies) (interface{}, error) {
30
+			var params struct {
31
+				Unit   string `json:"unit"`
32
+				Offset int    `json:"offset"`
33
+			}
34
+			log.Printf("AI 调用工具-get_date_range: %s", time.Now())
35
+			if len(input) > 0 {
36
+				if err := json.Unmarshal(input, &params); err != nil {
37
+					return nil, err
38
+				}
39
+			}
40
+
41
+			now := time.Now()
42
+			var startDate, endDate time.Time
43
+
44
+			switch params.Unit {
45
+			case "day":
46
+				targetDate := now.AddDate(0, 0, params.Offset)
47
+				startDate = targetDate
48
+				endDate = targetDate
49
+
50
+			case "week":
51
+				weekStart := getWeekStart(now)
52
+				targetWeekStart := weekStart.AddDate(0, 0, params.Offset*7)
53
+				targetWeekEnd := targetWeekStart.AddDate(0, 0, 6)
54
+				startDate = targetWeekStart
55
+				endDate = targetWeekEnd
56
+
57
+			case "month":
58
+				targetMonth := now.AddDate(0, params.Offset, 0)
59
+				monthStart := time.Date(targetMonth.Year(), targetMonth.Month(), 1, 0, 0, 0, 0, targetMonth.Location())
60
+				monthEnd := monthStart.AddDate(0, 1, -1)
61
+				startDate = monthStart
62
+				endDate = monthEnd
63
+
64
+			case "quarter":
65
+				// 基于当前季度偏移
66
+				currentMonth := int(now.Month())
67
+				currentQuarter := (currentMonth-1)/3 + 1
68
+				targetQuarter := currentQuarter + params.Offset
69
+				targetYear := now.Year()
70
+
71
+				// 处理跨年
72
+				if targetQuarter < 1 {
73
+					yearsBack := (1 - targetQuarter + 3) / 4
74
+					targetYear -= yearsBack
75
+					targetQuarter += yearsBack * 4
76
+				} else if targetQuarter > 4 {
77
+					yearsForward := (targetQuarter - 1) / 4
78
+					targetYear += yearsForward
79
+					targetQuarter -= yearsForward * 4
80
+				}
81
+
82
+				// 计算季度范围
83
+				quarterStartMonth := time.Month((targetQuarter-1)*3 + 1)
84
+				startDate = time.Date(targetYear, quarterStartMonth, 1, 0, 0, 0, 0, now.Location())
85
+				endDate = time.Date(targetYear, quarterStartMonth+3, 0, 0, 0, 0, 0, now.Location())
86
+
87
+			case "year":
88
+				targetYear := now.AddDate(params.Offset, 0, 0)
89
+				yearStart := time.Date(targetYear.Year(), 1, 1, 0, 0, 0, 0, targetYear.Location())
90
+				yearEnd := time.Date(targetYear.Year(), 12, 31, 0, 0, 0, 0, targetYear.Location())
91
+				startDate = yearStart
92
+				endDate = yearEnd
93
+
94
+			default:
95
+				startDate = now
96
+				endDate = now
97
+			}
98
+
99
+			return map[string]interface{}{
100
+				"unit":       params.Unit,
101
+				"offset":     params.Offset,
102
+				"start_date": startDate.Format("2006-01-02"),
103
+				"end_date":   endDate.Format("2006-01-02"),
104
+			}, nil
105
+		},
106
+	)
107
+}
108
+
109
+func getWeekStart(t time.Time) time.Time {
110
+	weekday := int(t.Weekday())
111
+	if weekday == 0 {
112
+		return t.AddDate(0, 0, -6)
113
+	}
114
+	return t.AddDate(0, 0, -(weekday - 1))
115
+}

+ 379
- 0
internal/tools/get_field_matcher.go Просмотреть файл

@@ -0,0 +1,379 @@
1
+package tools
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"sync"
7
+	"time"
8
+
9
+	"git.x2erp.com/qdy/go-svc-mcp/internal/mcp"
10
+)
11
+
12
+// 数据库字典表结构
13
+type FieldDictionary struct {
14
+	TableNameCN  string `json:"table_name_cn"`
15
+	TableNameEN  string `json:"table_name_en"`
16
+	FieldNameCN  string `json:"field_name_cn"`
17
+	FieldNameEN  string `json:"field_name_en"`
18
+	FieldType    string `json:"field_type"`
19
+	StandardName string `json:"standard_name"`
20
+	Description  string `json:"description"`
21
+	Category     string `json:"category"`
22
+	IsCalculated bool   `json:"is_calculated"`
23
+	Aliases      string `json:"aliases"`
24
+}
25
+
26
+// 缓存结构,添加过期时间和最后访问时间
27
+type fieldDictionaryCache struct {
28
+	data           []FieldDictionary
29
+	expireTime     time.Time     // 缓存过期时间
30
+	lastAccessTime time.Time     // 最后访问时间
31
+	expireDuration time.Duration // 过期时长
32
+	mutex          sync.RWMutex  // 读写锁
33
+}
34
+
35
+// 全局缓存实例
36
+var cache *fieldDictionaryCache
37
+
38
+// 初始化缓存
39
+func initCache() {
40
+	if cache == nil {
41
+		cache = &fieldDictionaryCache{
42
+			expireDuration: 20 * time.Minute, // 20分钟过期
43
+			data:           make([]FieldDictionary, 0),
44
+		}
45
+	}
46
+}
47
+
48
+// 获取缓存数据(会更新最后访问时间)
49
+func getCache() ([]FieldDictionary, bool) {
50
+	initCache()
51
+
52
+	cache.mutex.RLock()
53
+	defer cache.mutex.RUnlock()
54
+
55
+	// 检查缓存是否有效
56
+	if len(cache.data) == 0 {
57
+		return nil, false
58
+	}
59
+
60
+	// 检查是否过期(20分钟没有被访问)
61
+	if time.Since(cache.lastAccessTime) > cache.expireDuration {
62
+		return nil, false
63
+	}
64
+
65
+	// 检查是否到达过期时间
66
+	if time.Now().After(cache.expireTime) {
67
+		return nil, false
68
+	}
69
+
70
+	return cache.data, true
71
+}
72
+
73
+// 设置缓存数据
74
+func setCache(data []FieldDictionary) {
75
+	initCache()
76
+
77
+	cache.mutex.Lock()
78
+	defer cache.mutex.Unlock()
79
+
80
+	cache.data = data
81
+	cache.lastAccessTime = time.Now()
82
+	cache.expireTime = time.Now().Add(cache.expireDuration)
83
+}
84
+
85
+// 清除缓存
86
+func clearCache() {
87
+	initCache()
88
+
89
+	cache.mutex.Lock()
90
+	defer cache.mutex.Unlock()
91
+
92
+	cache.data = make([]FieldDictionary, 0)
93
+	cache.lastAccessTime = time.Time{}
94
+	cache.expireTime = time.Time{}
95
+}
96
+
97
+// 更新最后访问时间
98
+func updateLastAccessTime() {
99
+	initCache()
100
+
101
+	cache.mutex.Lock()
102
+	defer cache.mutex.Unlock()
103
+
104
+	cache.lastAccessTime = time.Now()
105
+}
106
+
107
+// 从数据库读取字段字典的方法
108
+func getFieldDictionaryFromDB() ([]FieldDictionary, error) {
109
+	// TODO: 你来实现这个数据库查询
110
+	// 这里返回示例数据用于测试
111
+
112
+	// 示例查询SQL(根据你的实际表结构调整):
113
+	/*
114
+		SELECT
115
+			table_name_cn,
116
+			table_name_en,
117
+			field_name_cn,
118
+			field_name_en,
119
+			field_type,
120
+			standard_name,
121
+			description,
122
+			category,
123
+			is_calculated,
124
+			aliases
125
+		FROM system_field_dictionary
126
+		WHERE tenant_id = ? AND is_active = true
127
+		ORDER BY category, table_name_cn, field_name_cn
128
+	*/
129
+
130
+	return []FieldDictionary{
131
+		// 销售相关字段
132
+		{
133
+			TableNameCN:  "销售订单",
134
+			TableNameEN:  "sales_order",
135
+			FieldNameCN:  "销售数量",
136
+			FieldNameEN:  "sales_quantity",
137
+			FieldType:    "decimal(10,2)",
138
+			StandardName: "销售数量",
139
+			Description:  "销售订单中的商品数量",
140
+			Category:     "销售",
141
+			IsCalculated: false,
142
+			Aliases:      "销量,销售数",
143
+		},
144
+		{
145
+			TableNameCN:  "销售订单",
146
+			TableNameEN:  "sales_order",
147
+			FieldNameCN:  "结算单价",
148
+			FieldNameEN:  "settlement_price",
149
+			FieldType:    "decimal(10,2)",
150
+			StandardName: "结算单价",
151
+			Description:  "销售结算时的单价",
152
+			Category:     "销售",
153
+			IsCalculated: false,
154
+			Aliases:      "单价,销售单价",
155
+		},
156
+		{
157
+			TableNameCN:  "销售订单",
158
+			TableNameEN:  "sales_order",
159
+			FieldNameCN:  "销售金额",
160
+			FieldNameEN:  "sales_amount",
161
+			FieldType:    "decimal(10,2)",
162
+			StandardName: "销售金额",
163
+			Description:  "销售总金额(计算字段:销售数量 × 结算单价)",
164
+			Category:     "销售",
165
+			IsCalculated: true,
166
+			Aliases:      "销售额,销售总额",
167
+		},
168
+		// 采购相关字段
169
+		{
170
+			TableNameCN:  "采购订单",
171
+			TableNameEN:  "purchase_order",
172
+			FieldNameCN:  "采购数量",
173
+			FieldNameEN:  "purchase_quantity",
174
+			FieldType:    "decimal(10,2)",
175
+			StandardName: "采购数量",
176
+			Description:  "采购订单中的商品数量",
177
+			Category:     "采购",
178
+			IsCalculated: false,
179
+			Aliases:      "进货数量,采购数",
180
+		},
181
+		{
182
+			TableNameCN:  "采购订单",
183
+			TableNameEN:  "purchase_order",
184
+			FieldNameCN:  "采购单价",
185
+			FieldNameEN:  "purchase_price",
186
+			FieldType:    "decimal(10,2)",
187
+			StandardName: "采购单价",
188
+			Description:  "采购商品单价",
189
+			Category:     "采购",
190
+			IsCalculated: false,
191
+			Aliases:      "进价,采购价",
192
+		},
193
+		// 库存相关字段
194
+		{
195
+			TableNameCN:  "库存表",
196
+			TableNameEN:  "inventory",
197
+			FieldNameCN:  "库存数量",
198
+			FieldNameEN:  "inventory_quantity",
199
+			FieldType:    "decimal(10,2)",
200
+			StandardName: "库存数量",
201
+			Description:  "当前库存数量",
202
+			Category:     "库存",
203
+			IsCalculated: false,
204
+			Aliases:      "库存,现存量",
205
+		},
206
+	}, nil
207
+}
208
+
209
+// 加载字段字典(带缓存)
210
+func loadFieldDictionary(refresh bool) ([]FieldDictionary, error) {
211
+	// 如果强制刷新,先清除缓存
212
+	if refresh {
213
+		clearCache()
214
+	}
215
+
216
+	// 尝试从缓存获取
217
+	if data, ok := getCache(); ok {
218
+		return data, nil
219
+	}
220
+
221
+	// 从数据库加载
222
+	dict, err := getFieldDictionaryFromDB()
223
+	if err != nil {
224
+		return nil, fmt.Errorf("加载字段字典失败: %v", err)
225
+	}
226
+
227
+	// 设置缓存
228
+	setCache(dict)
229
+
230
+	return dict, nil
231
+}
232
+
233
+// 查找字段匹配
234
+func findFieldMatch(fieldCN string, dictionary []FieldDictionary) (bool, *FieldDictionary, []string) {
235
+	// 精确匹配
236
+	for _, dict := range dictionary {
237
+		if dict.FieldNameCN == fieldCN {
238
+			return true, &dict, nil
239
+		}
240
+	}
241
+
242
+	// 别名匹配
243
+	var matchedDict *FieldDictionary
244
+	for _, dict := range dictionary {
245
+		if dict.Aliases != "" {
246
+			// 简单的别名匹配(实际使用时你可能需要解析逗号分隔的字符串)
247
+			if dict.Aliases == fieldCN {
248
+				matchedDict = &dict
249
+				break
250
+			}
251
+		}
252
+	}
253
+
254
+	if matchedDict != nil {
255
+		return true, matchedDict, nil
256
+	}
257
+
258
+	// 收集所有字段名作为建议
259
+	var suggestions []string
260
+	for _, dict := range dictionary {
261
+		suggestions = append(suggestions, dict.FieldNameCN)
262
+	}
263
+
264
+	return false, nil, suggestions
265
+}
266
+
267
+func init() {
268
+	mcp.Register("field_matcher", "根据中文字段名称匹配数据库字段信息,返回英文字段名、表名、类型等详细信息",
269
+		map[string]interface{}{
270
+			"type": "object",
271
+			"properties": map[string]interface{}{
272
+				"fields": map[string]interface{}{
273
+					"type": "array",
274
+					"items": map[string]interface{}{
275
+						"type": "string",
276
+					},
277
+					"description": "要匹配的中文字段名称数组",
278
+					"minItems":    1,
279
+				},
280
+				"refresh_cache": map[string]interface{}{
281
+					"type":        "boolean",
282
+					"description": "是否刷新字段字典缓存",
283
+					"default":     false,
284
+				},
285
+			},
286
+			"required": []string{"fields"},
287
+		},
288
+		func(input json.RawMessage, deps *mcp.ToolDependencies) (interface{}, error) {
289
+			var params struct {
290
+				Fields       []string `json:"fields"`
291
+				RefreshCache bool     `json:"refresh_cache"`
292
+			}
293
+
294
+			if len(input) > 0 {
295
+				if err := json.Unmarshal(input, &params); err != nil {
296
+					return nil, err
297
+				}
298
+			}
299
+
300
+			if len(params.Fields) == 0 {
301
+				return nil, fmt.Errorf("fields 参数不能为空")
302
+			}
303
+
304
+			// 加载字段字典(带缓存)
305
+			startTime := time.Now()
306
+			dictionary, err := loadFieldDictionary(params.RefreshCache)
307
+			if err != nil {
308
+				return nil, err
309
+			}
310
+			loadTime := time.Since(startTime)
311
+
312
+			// 更新缓存最后访问时间
313
+			updateLastAccessTime()
314
+
315
+			// 处理每个字段的匹配结果
316
+			matches := make([]map[string]interface{}, 0, len(params.Fields))
317
+			foundCount := 0
318
+
319
+			for _, fieldCN := range params.Fields {
320
+				isFound, fieldInfo, suggestions := findFieldMatch(fieldCN, dictionary)
321
+
322
+				matchResult := map[string]interface{}{
323
+					"input_field_cn": fieldCN,
324
+					"is_found":       isFound,
325
+				}
326
+
327
+				if isFound && fieldInfo != nil {
328
+					foundCount++
329
+					matchResult["match_info"] = map[string]interface{}{
330
+						"table_name_cn": fieldInfo.TableNameCN,
331
+						"table_name_en": fieldInfo.TableNameEN,
332
+						"field_name_en": fieldInfo.FieldNameEN,
333
+						"field_type":    fieldInfo.FieldType,
334
+						"standard_name": fieldInfo.StandardName,
335
+						"description":   fieldInfo.Description,
336
+						"category":      fieldInfo.Category,
337
+						"is_calculated": fieldInfo.IsCalculated,
338
+					}
339
+				} else {
340
+					matchResult["suggestions"] = suggestions
341
+					matchResult["note"] = "未找到匹配字段,请尝试其他名称或拆解字段"
342
+				}
343
+
344
+				matches = append(matches, matchResult)
345
+			}
346
+
347
+			// 获取缓存信息
348
+			cacheData, cacheValid := getCache()
349
+			cacheInfo := map[string]interface{}{
350
+				"is_valid":     cacheValid,
351
+				"total_fields": len(cacheData),
352
+			}
353
+
354
+			if cacheValid {
355
+				cacheInfo["last_access_time"] = cache.lastAccessTime.Format(time.RFC3339)
356
+				cacheInfo["expire_time"] = cache.expireTime.Format(time.RFC3339)
357
+				cacheInfo["will_expire_in"] = time.Until(cache.expireTime).String()
358
+			}
359
+
360
+			// 按照示例的返回格式
361
+			return map[string]interface{}{
362
+				"tenant_id":    deps.ReqCtx.TenantID,
363
+				"user_id":      deps.ReqCtx.UserID,
364
+				"input_fields": params.Fields,
365
+				"matches":      matches,
366
+				"summary": map[string]interface{}{
367
+					"total_fields":    len(params.Fields),
368
+					"found_count":     foundCount,
369
+					"not_found_count": len(params.Fields) - foundCount,
370
+					"success_rate":    fmt.Sprintf("%.1f%%", float64(foundCount)/float64(len(params.Fields))*100),
371
+				},
372
+				"cache_info": cacheInfo,
373
+				"load_time":  loadTime.String(),
374
+				"timestamp":  time.Now().Format(time.RFC3339),
375
+				"suggestion": "如果未找到匹配字段,请尝试将复合字段拆解为ERP常用基本字段再次查询",
376
+			}, nil
377
+		},
378
+	)
379
+}

+ 68
- 0
internal/tools/user_count.go Просмотреть файл

@@ -0,0 +1,68 @@
1
+package tools
2
+
3
+import (
4
+	"encoding/json"
5
+
6
+	"git.x2erp.com/qdy/go-svc-mcp/internal/mcp"
7
+)
8
+
9
+func init() {
10
+	mcp.Register("get_user_count", "获取用户数量,演示数据库工厂和上下文注入",
11
+		map[string]interface{}{
12
+			"type": "object",
13
+			"properties": map[string]interface{}{
14
+				"active_only": map[string]interface{}{
15
+					"type":        "boolean",
16
+					"description": "是否只统计活跃用户",
17
+					"default":     false,
18
+				},
19
+			},
20
+			"required": []string{},
21
+		},
22
+		func(input json.RawMessage, deps *mcp.ToolDependencies) (interface{}, error) {
23
+			var params struct {
24
+				ActiveOnly bool `json:"active_only"`
25
+			}
26
+			if len(input) > 0 {
27
+				if err := json.Unmarshal(input, &params); err != nil {
28
+					return nil, err
29
+				}
30
+			}
31
+
32
+			// 使用数据库工厂执行查询(示例)
33
+			var count int64
34
+
35
+			if deps.DBFactory != nil {
36
+				// 实际项目中,这里会执行数据库查询
37
+				// 示例:假设我们有一个用户表
38
+				// db := deps.DBFactory.GetDB()
39
+				// query := "SELECT COUNT(*) FROM users WHERE tenant_id = ?"
40
+				// if params.ActiveOnly {
41
+				//     query += " AND status = 'active'"
42
+				// }
43
+				// err := db.QueryRow(query, deps.ReqCtx.TenantID).Scan(&count)
44
+
45
+				// 模拟查询结果
46
+
47
+				count = 42
48
+				if params.ActiveOnly {
49
+					count = 25
50
+				}
51
+			} else {
52
+				// 无数据库工厂,返回模拟数据
53
+				count = 100
54
+				if params.ActiveOnly {
55
+					count = 60
56
+				}
57
+			}
58
+
59
+			return map[string]interface{}{
60
+				"tenant_id":   deps.ReqCtx.TenantID,
61
+				"user_count":  count,
62
+				"active_only": params.ActiveOnly,
63
+				"has_db":      deps.DBFactory != nil,
64
+				"timestamp":   "2024-01-01T00:00:00Z", // 实际应使用 time.Now()
65
+			}, nil
66
+		},
67
+	)
68
+}

+ 90
- 0
main.go Просмотреть файл

@@ -0,0 +1,90 @@
1
+package main
2
+
3
+import (
4
+	"log"
5
+	"net/http"
6
+	"os"
7
+
8
+	"git.x2erp.com/qdy/go-svc-mcp/internal/auth"
9
+	"git.x2erp.com/qdy/go-svc-mcp/internal/mcp"
10
+	_ "git.x2erp.com/qdy/go-svc-mcp/internal/tools" // 触发工具自动注册
11
+
12
+	"git.x2erp.com/qdy/go-base/config"
13
+	"git.x2erp.com/qdy/go-base/container"
14
+	"git.x2erp.com/qdy/go-base/ctx"
15
+	"git.x2erp.com/qdy/go-base/graceful"
16
+	"git.x2erp.com/qdy/go-base/logger"
17
+	"git.x2erp.com/qdy/go-db/factory/database"
18
+	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
19
+)
20
+
21
+var (
22
+	appName    = "svc-mcp"
23
+	appVersion = "1"
24
+)
25
+
26
+func main() {
27
+	// 0. 初始化日志
28
+	logBootFactory := logger.InitBootLog()
29
+
30
+	// 1. 获取配置文件
31
+	cfg := config.GetConfig()
32
+	cfg.SetAppName(appName)
33
+	cfg.SetAppVersion(appVersion)
34
+
35
+	// 2. 创建关闭容器
36
+	ctr := container.NewContainer(cfg)
37
+
38
+	// 注册日志,实现自动关闭
39
+	container.Reg(ctr, logBootFactory)
40
+
41
+	// 3. 创建数据库工厂
42
+	dbFactory := container.Create(ctr, database.CreateDBFactory)
43
+	dbFactory.TestConnection()
44
+
45
+	// 4. 创建基础请求上下文(可从配置或认证中提取)
46
+	baseCtx := &ctx.RequestContext{
47
+		TenantID: "default-tenant", // 实际应从认证中间件获取
48
+	}
49
+
50
+	// 5. 创建 MCP 服务器
51
+	mcpServer, err := mcp.NewServer(mcp.Config{
52
+		Name:        appName,
53
+		Version:     appVersion,
54
+		Description: "MCP 工具服务,提供自动注册发现和依赖注入",
55
+		DBFactory:   dbFactory,
56
+		BaseCtx:     baseCtx,
57
+		Port:        cfg.GetServiceConfig().Port,
58
+		ServiceName: cfg.GetServiceConfig().ServiceName,
59
+	})
60
+	if err != nil {
61
+		log.Fatalf("Failed to create MCP server: %v", err)
62
+	}
63
+	log.Printf("MCP server created with tools registered")
64
+
65
+	// 6. 获取 SDK 服务器实例
66
+	sdkServer := mcpServer.GetSDKServer()
67
+
68
+	// 7. 创建 HTTP 处理器(带验证中间件)
69
+	authToken := os.Getenv("MCP_AUTH_TOKEN")
70
+	if authToken == "" {
71
+		authToken = "123" // 仅用于开发,生产环境必须设置
72
+	}
73
+	projectIDHeader := os.Getenv("MCP_PROJECT_ID_HEADER")
74
+	if projectIDHeader == "" {
75
+		projectIDHeader = "X-Project-ID"
76
+	}
77
+	mcpHandler := mcpsdk.NewStreamableHTTPHandler(func(req *http.Request) *mcpsdk.Server {
78
+		return sdkServer
79
+	}, nil)
80
+	// 包装验证中间件
81
+	handler := auth.AuthMiddleware(mcpHandler, authToken, projectIDHeader)
82
+
83
+	mcpServer.Run(handler)
84
+
85
+	//启用运行日志
86
+	container.Create(ctr, logger.InitRuntimeLogger)
87
+
88
+	//等待关闭
89
+	graceful.WaitForShutdown(appName, mcpServer.GetHTTPServer(), ctr)
90
+}

Загрузка…
Отмена
Сохранить