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:
Bryan Frimin
2025-09-07 22:13:45 +02:00
parent d71800efbd
commit 1436d0db08
6 changed files with 382 additions and 0 deletions

View File

@@ -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)
}

View 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
}

View 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
}

View 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
}