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:
@@ -30,6 +30,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/saferedirect"
|
||||
console_v1 "go.probo.inc/probo/pkg/server/api/console/v1"
|
||||
mcp_v1 "go.probo.inc/probo/pkg/server/api/mcp/v1"
|
||||
trust_v1 "go.probo.inc/probo/pkg/server/api/trust/v1"
|
||||
"go.probo.inc/probo/pkg/trust"
|
||||
)
|
||||
@@ -207,5 +208,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Mount the trust API with authentication
|
||||
router.Mount("/trust/v1", s.trustAPIHandler)
|
||||
|
||||
// Mount the MCP API - use Route instead of Mount to preserve path for handler
|
||||
router.Mount(
|
||||
"/mcp/v1",
|
||||
mcp_v1.NewMux(s.cfg.Probo),
|
||||
)
|
||||
|
||||
router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
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