add mcp (Model Context Protocol) support

This commit is contained in:
MaysWind
2025-07-06 03:02:19 +08:00
parent 620ccf317f
commit 8dce0f2d6a
32 changed files with 1379 additions and 20 deletions
+3
View File
@@ -15,6 +15,9 @@ type MiddlewareHandlerFunc func(*WebContext)
// ApiHandlerFunc represents the api handler function
type ApiHandlerFunc func(*WebContext) (any, *errs.Error)
// JSONRPCApiHandlerFunc represents the api handler function
type JSONRPCApiHandlerFunc func(*WebContext, *JSONRPCRequest) (any, *errs.Error)
// EventStreamApiHandlerFunc represents the event stream api handler function
type EventStreamApiHandlerFunc func(*WebContext) *errs.Error
+177
View File
@@ -0,0 +1,177 @@
package core
import (
"regexp"
"strconv"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
// IPPattern represents a pattern for matching IP addresses, either IPv4 or IPv6
type IPPattern struct {
Pattern string
regex *regexp.Regexp
}
// Match returns if the given IP address matches the pattern
func (p *IPPattern) Match(ip string) bool {
if p.regex == nil {
return false
}
return p.regex.MatchString(ip)
}
// GobEncode returns the encoded data for this IP pattern
func (p *IPPattern) GobEncode() ([]byte, error) {
return []byte(p.Pattern), nil
}
// GobDecode decodes the data into the IP pattern
func (p *IPPattern) GobDecode(data []byte) error {
pattern := string(data)
if pattern == "" {
p.Pattern = ""
p.regex = nil
return nil
}
newPattern, err := ParseIPPattern(pattern)
if err != nil {
return err
}
p.Pattern = newPattern.Pattern
p.regex = newPattern.regex
return nil
}
// ParseIPPattern parses the given IP address pattern and returns an IPPattern object
func ParseIPPattern(ipPattern string) (*IPPattern, error) {
if ipPattern == "" {
return nil, nil
}
hasDot := false
hasSemicolon := false
for i := 0; i < len(ipPattern); i++ {
ch := rune(ipPattern[i])
if ch == '.' { // may be IPv4
if hasSemicolon {
return nil, errs.ErrInvalidIpAddressPattern
}
hasDot = true
} else if ch == ':' { // may be IPv6
if hasDot {
return nil, errs.ErrInvalidIpAddressPattern
}
hasSemicolon = true
}
}
if hasDot {
return ParseIPv4Pattern(ipPattern)
} else if hasSemicolon {
return ParseIPv6Pattern(ipPattern)
} else {
return nil, errs.ErrInvalidIpAddressPattern
}
}
// ParseIPv4Pattern parses the given IPv4 address pattern and returns an IPPattern object
func ParseIPv4Pattern(ipPattern string) (*IPPattern, error) {
items := strings.Split(ipPattern, ".")
if len(items) != 4 {
return nil, errs.ErrInvalidIpAddressPattern
}
regexBuilder := strings.Builder{}
regexBuilder.WriteRune('^')
for i := 0; i < len(items); i++ {
item := strings.TrimSpace(items[i])
if item == "*" {
regexBuilder.WriteString("[0-9]{1,3}")
} else if item == "" {
return nil, errs.ErrInvalidIpAddressPattern
} else {
num, err := strconv.Atoi(item)
if err != nil || num < 0 || num > 255 {
return nil, errs.ErrInvalidIpAddressPattern
}
regexBuilder.WriteString(item)
}
if i < len(items)-1 {
regexBuilder.WriteRune('\\')
regexBuilder.WriteRune('.')
}
}
regexBuilder.WriteRune('$')
regex, err := regexp.Compile(regexBuilder.String())
if err != nil {
return nil, errs.ErrInvalidIpAddressPattern
}
return &IPPattern{
Pattern: ipPattern,
regex: regex,
}, nil
}
// ParseIPv6Pattern parses the given IPv6 address pattern and returns an IPPattern object
func ParseIPv6Pattern(ipPattern string) (*IPPattern, error) {
items := strings.Split(ipPattern, ":")
if len(items) < 2 || len(items) > 8 {
return nil, errs.ErrInvalidIpAddressPattern
}
regexBuilder := strings.Builder{}
regexBuilder.WriteRune('^')
for i := 0; i < len(items); i++ {
item := strings.TrimSpace(items[i])
if item == "*" {
regexBuilder.WriteString("[0-9a-fA-F]{1,4}")
} else if i < len(items)-1 && item == "" {
// Do Nothing
} else {
num, err := strconv.ParseInt(item, 16, 32)
if err != nil || num < 0 || num > 0xFFFF {
return nil, errs.ErrInvalidIpAddressPattern
}
regexBuilder.WriteString(item)
}
if i < len(items)-1 {
regexBuilder.WriteRune(':')
}
}
regexBuilder.WriteRune('$')
regex, err := regexp.Compile(regexBuilder.String())
if err != nil {
return nil, errs.ErrInvalidIpAddressPattern
}
return &IPPattern{
Pattern: ipPattern,
regex: regex,
}, nil
}
+135
View File
@@ -0,0 +1,135 @@
package core
import (
"bytes"
"encoding/gob"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
func TestIPPattern_GobEncode(t *testing.T) {
pattern, err := ParseIPPattern("192.168.1.*")
assert.Nil(t, err)
assert.NotNil(t, pattern)
var buf bytes.Buffer
err = gob.NewEncoder(&buf).Encode(pattern)
assert.Nil(t, err)
newPattern := &IPPattern{}
err = gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(newPattern)
assert.Nil(t, err)
assert.NotNil(t, newPattern)
assert.Equal(t, pattern.Pattern, newPattern.Pattern)
assert.Equal(t, pattern.regex.String(), newPattern.regex.String())
assert.True(t, newPattern.Match("192.168.1.1"))
assert.True(t, newPattern.Match("192.168.1.255"))
}
func TestParseIPPattern(t *testing.T) {
pattern, err := ParseIPPattern("")
assert.Nil(t, err)
assert.Nil(t, pattern)
pattern, err = ParseIPPattern("invalid")
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
pattern, err = ParseIPPattern("192.1:2:3.4")
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
pattern, err = ParseIPPattern("0:0:0:0:0:0:1.2.3.4") // not support IPv6 with embedded IPv4
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
pattern, err = ParseIPPattern("192.168.1.*")
assert.Nil(t, err)
assert.NotNil(t, pattern)
assert.True(t, pattern.Match("192.168.1.1"))
assert.True(t, pattern.Match("192.168.1.255"))
assert.False(t, pattern.Match("192.168.2.1"))
pattern, err = ParseIPPattern("2001:db8::*")
assert.Nil(t, err)
assert.NotNil(t, pattern)
assert.True(t, pattern.Match("2001:db8::1"))
assert.True(t, pattern.Match("2001:db8::ffff"))
assert.False(t, pattern.Match("2001:db9::1"))
}
func TestParseIPv4Pattern(t *testing.T) {
pattern, err := ParseIPv4Pattern("192.168.1.1")
assert.Nil(t, err)
assert.NotNil(t, pattern)
assert.True(t, pattern.Match("192.168.1.1"))
assert.False(t, pattern.Match("192.168.1.2"))
pattern, err = ParseIPv4Pattern("192.168.*.1")
assert.Nil(t, err)
assert.NotNil(t, pattern)
assert.True(t, pattern.Match("192.168.1.1"))
assert.True(t, pattern.Match("192.168.255.1"))
assert.False(t, pattern.Match("192.168.1.2"))
pattern, err = ParseIPv4Pattern("*.*.*.*")
assert.Nil(t, err)
assert.NotNil(t, pattern)
assert.True(t, pattern.Match("0.0.0.0"))
assert.True(t, pattern.Match("255.255.255.255"))
pattern, err = ParseIPv4Pattern("256.256.256.256")
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
pattern, err = ParseIPv4Pattern("1.2.3")
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
pattern, err = ParseIPv4Pattern("1.2.3.4.5")
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
pattern, err = ParseIPv4Pattern("a.b.c.d")
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
}
func TestParseIPv6Pattern(t *testing.T) {
pattern, err := ParseIPv6Pattern("2001:db8:85a3:8d3:1319:8a2e:370:7348")
assert.Nil(t, err)
assert.NotNil(t, pattern)
assert.True(t, pattern.Match("2001:db8:85a3:8d3:1319:8a2e:370:7348"))
assert.False(t, pattern.Match("2001:db8:85a3:8d3:1319:8a2e:370:7349"))
pattern, err = ParseIPv6Pattern("2001:db8::*")
assert.Nil(t, err)
assert.NotNil(t, pattern)
assert.True(t, pattern.Match("2001:db8::0"))
assert.True(t, pattern.Match("2001:db8::ffff"))
assert.False(t, pattern.Match("2001:db9::0"))
pattern, err = ParseIPv6Pattern("::*")
assert.Nil(t, err)
assert.NotNil(t, pattern)
assert.True(t, pattern.Match("::1"))
assert.True(t, pattern.Match("::2"))
assert.False(t, pattern.Match(":1:1"))
pattern, err = ParseIPv6Pattern("2001:db8:85a3:8d3:1319:8a2e:370:7348:extra")
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
pattern, err = ParseIPv6Pattern("g001:db8:85a3:8d3")
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
pattern, err = ParseIPv6Pattern("2001:db8:")
assert.Equal(t, errs.ErrInvalidIpAddressPattern, err)
assert.Nil(t, pattern)
}
+95
View File
@@ -0,0 +1,95 @@
package core
import "encoding/json"
// JSONRPCVersion defines the version of JSON-RPC protocol
const JSONRPCVersion = "2.0"
// JSONRPCRequest represents the JSON-RPC 2.0 request
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
ID any `json:"id,omitempty"`
}
// JSONRPCResponse represents the JSON-RPC 2.0 response
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result any `json:"result,omitempty"`
Error *JSONRPCError `json:"error,omitempty"`
ID any `json:"id,omitempty"`
}
// JSONRPCError represents the JSON-RPC 2.0 error object
type JSONRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
// JSONRPCParseError represents the "Parse error" in JSON-RPC 2.0
var JSONRPCParseError = &JSONRPCError{
Code: -32700,
Message: "Parse error",
Data: nil,
}
// JSONRPCMethodNotFoundError represents the "Method not found" error in JSON-RPC 2.0
var JSONRPCMethodNotFoundError = &JSONRPCError{
Code: -32601,
Message: "Method not found",
Data: nil,
}
// JSONRPCInvalidParamsError represents the "Invalid params" error in JSON-RPC 2.0
var JSONRPCInvalidParamsError = &JSONRPCError{
Code: -32602,
Message: "Invalid params",
Data: nil,
}
// JSONRPCInternalError represents the "Internal error" in JSON-RPC 2.0
var JSONRPCInternalError = &JSONRPCError{
Code: -32603,
Message: "Internal error",
Data: nil,
}
// NewJSONRPCResponse creates a new JSON-RPC response with the result
func NewJSONRPCResponse(id any, result any) *JSONRPCResponse {
return &JSONRPCResponse{
JSONRPC: JSONRPCVersion,
Result: result,
Error: nil,
ID: id,
}
}
// NewJSONRPCErrorResponse creates a new JSON-RPC error response
func NewJSONRPCErrorResponse(id any, err *JSONRPCError) *JSONRPCResponse {
return &JSONRPCResponse{
JSONRPC: JSONRPCVersion,
Result: nil,
Error: &JSONRPCError{
Code: err.Code,
Message: err.Message,
Data: nil,
},
ID: id,
}
}
// NewJSONRPCErrorResponseWithCause creates a new JSON-RPC error response
func NewJSONRPCErrorResponseWithCause(id any, err *JSONRPCError, cause string) *JSONRPCResponse {
return &JSONRPCResponse{
JSONRPC: JSONRPCVersion,
Result: nil,
Error: &JSONRPCError{
Code: err.Code,
Message: err.Message,
Data: cause,
},
ID: id,
}
}