Add simple MCP server
Proof of concept of working MCP server for Probo. Currently the official MCP library does not support session that why the server is configured in stateless mode. It seams the input jsonschema is not used to perform any validation, so we should figuring out how to validate the input properly. Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
87
pkg/server/api/mcp/v1/add_vendor.go
Normal file
87
pkg/server/api/mcp/v1/add_vendor.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/getprobo/probo/pkg/coredata"
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/probo"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type (
|
||||
addVendorArgs struct {
|
||||
Name string
|
||||
Description *string
|
||||
HeadquarterAddress *string
|
||||
LegalName *string
|
||||
WebsiteURL *string
|
||||
Category *coredata.VendorCategory
|
||||
PrivacyPolicyURL *string
|
||||
ServiceLevelAgreementURL *string
|
||||
DataProcessingAgreementURL *string
|
||||
BusinessAssociateAgreementURL *string
|
||||
SubprocessorsListURL *string
|
||||
Certifications []string
|
||||
SecurityPageURL *string
|
||||
TrustPageURL *string
|
||||
TermsOfServiceURL *string
|
||||
StatusPageURL *string
|
||||
BusinessOwnerID *gid.GID
|
||||
SecurityOwnerID *gid.GID
|
||||
}
|
||||
|
||||
addVendorResult struct {
|
||||
Result struct {
|
||||
Name string
|
||||
ID string
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func (r *resolver) AddVendor(
|
||||
ctx context.Context,
|
||||
req *mcp.CallToolRequest,
|
||||
args *addVendorArgs,
|
||||
) (*mcp.CallToolResult, *addVendorResult, error) {
|
||||
vendor, err := r.proboSvc.Vendors.Create(
|
||||
ctx,
|
||||
probo.CreateVendorRequest{
|
||||
OrganizationID: r.organizationID,
|
||||
Name: args.Name,
|
||||
Description: args.Description,
|
||||
HeadquarterAddress: args.HeadquarterAddress,
|
||||
LegalName: args.LegalName,
|
||||
WebsiteURL: args.WebsiteURL,
|
||||
Category: args.Category,
|
||||
PrivacyPolicyURL: args.PrivacyPolicyURL,
|
||||
ServiceLevelAgreementURL: args.ServiceLevelAgreementURL,
|
||||
DataProcessingAgreementURL: args.DataProcessingAgreementURL,
|
||||
BusinessAssociateAgreementURL: args.BusinessAssociateAgreementURL,
|
||||
SubprocessorsListURL: args.SubprocessorsListURL,
|
||||
Certifications: args.Certifications,
|
||||
SecurityPageURL: args.SecurityPageURL,
|
||||
TrustPageURL: args.TrustPageURL,
|
||||
TermsOfServiceURL: args.TermsOfServiceURL,
|
||||
StatusPageURL: args.StatusPageURL,
|
||||
BusinessOwnerID: args.BusinessOwnerID,
|
||||
SecurityOwnerID: args.SecurityOwnerID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to list vendors: %w", err)
|
||||
}
|
||||
|
||||
result := &addVendorResult{
|
||||
Result: struct {
|
||||
Name string
|
||||
ID string
|
||||
}{
|
||||
Name: vendor.Name,
|
||||
ID: vendor.ID.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return nil, result, nil
|
||||
}
|
||||
70
pkg/server/api/mcp/v1/list_vendors.go
Normal file
70
pkg/server/api/mcp/v1/list_vendors.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/getprobo/probo/pkg/coredata"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type (
|
||||
listVendorsArgs struct {
|
||||
OrderField coredata.VendorOrderField
|
||||
Cursor *page.CursorKey
|
||||
Size int
|
||||
}
|
||||
|
||||
listVendorsResult struct {
|
||||
NextCursor *string
|
||||
Result []struct {
|
||||
Name string
|
||||
ID string
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func (r *resolver) ListVendors(
|
||||
ctx context.Context,
|
||||
req *mcp.CallToolRequest,
|
||||
args *listVendorsArgs,
|
||||
) (*mcp.CallToolResult, *listVendorsResult, error) {
|
||||
|
||||
filter := coredata.NewVendorFilter(nil, nil)
|
||||
cursor := page.NewCursor(
|
||||
args.Size,
|
||||
args.Cursor,
|
||||
page.Head,
|
||||
page.OrderBy[coredata.VendorOrderField]{
|
||||
Field: args.OrderField,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
},
|
||||
)
|
||||
|
||||
vendors, err := r.proboSvc.Vendors.ListForOrganizationID(ctx, r.organizationID, cursor, filter)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to list vendors: %w", err)
|
||||
}
|
||||
|
||||
result := &listVendorsResult{}
|
||||
if len(vendors.Data) > 0 {
|
||||
nextCursorKey := vendors.Data[len(vendors.Data)-1].CursorKey(args.OrderField).String()
|
||||
result.NextCursor = &nextCursorKey
|
||||
}
|
||||
|
||||
for _, vendor := range vendors.Data {
|
||||
result.Result = append(
|
||||
result.Result,
|
||||
struct {
|
||||
Name string
|
||||
ID string
|
||||
}{
|
||||
Name: vendor.Name,
|
||||
ID: vendor.ID.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return nil, result, nil
|
||||
}
|
||||
209
pkg/server/api/mcp/v1/v1_handler.go
Normal file
209
pkg/server/api/mcp/v1/v1_handler.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/probo"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type (
|
||||
resolver struct {
|
||||
proboSvc *probo.TenantService
|
||||
organizationID gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func NewMux(proboSvc *probo.Service) *chi.Mux {
|
||||
server := mcp.NewServer(
|
||||
&mcp.Implementation{
|
||||
Name: "probo",
|
||||
Title: "Probo",
|
||||
Version: "1.0.0", // todo retrieve from build info
|
||||
},
|
||||
&mcp.ServerOptions{},
|
||||
)
|
||||
|
||||
tenantID, err := gid.ParseTenantID("lXdXZSh-AAE")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
organizationID, err := gid.ParseGID("lXdXZSh-AAEAAAAAAZfLJi38a0AGbu37")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resolver := &resolver{proboSvc: proboSvc.WithTenant(tenantID), organizationID: organizationID}
|
||||
|
||||
mcp.AddTool(
|
||||
server,
|
||||
&mcp.Tool{
|
||||
Title: "List Vendors",
|
||||
Description: "List all vendors for the organization",
|
||||
Name: "listVendors",
|
||||
Annotations: &mcp.ToolAnnotations{
|
||||
Title: "List Vendors",
|
||||
ReadOnlyHint: true,
|
||||
},
|
||||
InputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"orderField": {
|
||||
Type: "string",
|
||||
Default: json.RawMessage(`"NAME"`),
|
||||
Enum: []any{
|
||||
"NAME",
|
||||
"CREATED_AT",
|
||||
"UPDATED_AT",
|
||||
},
|
||||
},
|
||||
"cursor": {
|
||||
Type: "string",
|
||||
},
|
||||
"size": {
|
||||
Type: "integer",
|
||||
Minimum: jsonschema.Ptr(float64(1)),
|
||||
Maximum: jsonschema.Ptr(float64(1000)),
|
||||
Default: json.RawMessage(`100`),
|
||||
},
|
||||
},
|
||||
},
|
||||
OutputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"result": {
|
||||
Type: "array",
|
||||
Items: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Required: []string{"name", "id"},
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"name": {
|
||||
Type: "string",
|
||||
},
|
||||
"id": {
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolver.ListVendors,
|
||||
)
|
||||
|
||||
mcp.AddTool(
|
||||
server,
|
||||
&mcp.Tool{
|
||||
Name: "addVendor",
|
||||
Description: "Add a vendor",
|
||||
InputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"name": {
|
||||
Type: "string",
|
||||
Required: []string{"name"},
|
||||
},
|
||||
"description": {
|
||||
Type: "string",
|
||||
},
|
||||
"headquarterAddress": {
|
||||
Type: "string",
|
||||
},
|
||||
"legalName": {
|
||||
Type: "string",
|
||||
},
|
||||
"websiteURL": {
|
||||
Type: "string",
|
||||
Format: "uri",
|
||||
},
|
||||
"category": {
|
||||
Type: "string",
|
||||
},
|
||||
"privacyPolicyURL": {
|
||||
Type: "string",
|
||||
},
|
||||
"serviceLevelAgreementURL": {
|
||||
Type: "string",
|
||||
Format: "uri",
|
||||
},
|
||||
"dataProcessingAgreementURL": {
|
||||
Type: "string",
|
||||
Format: "uri",
|
||||
},
|
||||
"businessAssociateAgreementURL": {
|
||||
Type: "string",
|
||||
Format: "uri",
|
||||
},
|
||||
"subprocessorsListURL": {
|
||||
Type: "string",
|
||||
Format: "uri",
|
||||
},
|
||||
"certifications": {
|
||||
Type: "array",
|
||||
Items: &jsonschema.Schema{
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
"securityPageURL": {
|
||||
Type: "string",
|
||||
Format: "uri",
|
||||
},
|
||||
"trustPageURL": {
|
||||
Type: "string",
|
||||
Format: "uri",
|
||||
},
|
||||
"termsOfServiceURL": {
|
||||
Type: "string",
|
||||
Format: "uri",
|
||||
},
|
||||
"statusPageURL": {
|
||||
Type: "string",
|
||||
Format: "uri",
|
||||
},
|
||||
"businessOwnerID": {
|
||||
Type: "string",
|
||||
},
|
||||
"securityOwnerID": {
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
OutputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"result": {
|
||||
Type: "object",
|
||||
Required: []string{"name", "id"},
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"name": {
|
||||
Type: "string",
|
||||
},
|
||||
"id": {
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolver.AddVendor,
|
||||
)
|
||||
|
||||
getServer := func(r *http.Request) *mcp.Server { return server }
|
||||
|
||||
handler := mcp.NewStreamableHTTPHandler(
|
||||
getServer,
|
||||
&mcp.StreamableHTTPOptions{Stateless: true},
|
||||
)
|
||||
|
||||
r := chi.NewMux()
|
||||
r.Handle("/", handler)
|
||||
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user