feat: publish plugin MCP descriptors

This commit is contained in:
Alex Dunmow 2026-08-09 13:00:33 +08:00
parent bf72cd160a
commit 911f3f70e0
5 changed files with 531 additions and 91 deletions

View File

@ -156,6 +156,26 @@ message PluginManifest {
// HTTP handler) and merges them into the site sitemap. Requires // HTTP handler) and merges them into the site sitemap. Requires
// has_http_handler. // has_http_handler.
bool sitemap = 37; bool sitemap = 37;
// mcp_tools is the descriptor-derived MCP surface for the plugin's own
// unary ConnectRPC methods. The Go guest binding captures these during
// DESCRIBE from the compiled protobuf descriptors; plugin authors do not
// duplicate schemas or procedure metadata by hand. The host still checks
// every procedure against rbac_method_roles before advertising or invoking
// it, so descriptor metadata cannot widen the plugin's authorization map.
repeated MCPToolDescriptor mcp_tools = 38;
}
// MCPToolDescriptor is one unary ConnectRPC method prepared for the host MCP
// catalogue. Procedure, service, method, documentation, and the request JSON
// schema are all derived from protobuf reflection during DESCRIBE.
message MCPToolDescriptor {
string procedure = 1;
string service_full_name = 2;
string method_name = 3;
string description = 4;
string documentation = 5;
bytes input_schema_json = 6;
} }
// PublicRoute is one site-root path claim (see PluginManifest.public_routes). // PublicRoute is one site-root path claim (see PluginManifest.public_routes).

View File

@ -150,6 +150,13 @@ type PluginManifest struct {
// HTTP handler) and merges them into the site sitemap. Requires // HTTP handler) and merges them into the site sitemap. Requires
// has_http_handler. // has_http_handler.
Sitemap bool `protobuf:"varint,37,opt,name=sitemap,proto3" json:"sitemap,omitempty"` Sitemap bool `protobuf:"varint,37,opt,name=sitemap,proto3" json:"sitemap,omitempty"`
// mcp_tools is the descriptor-derived MCP surface for the plugin's own
// unary ConnectRPC methods. The Go guest binding captures these during
// DESCRIBE from the compiled protobuf descriptors; plugin authors do not
// duplicate schemas or procedure metadata by hand. The host still checks
// every procedure against rbac_method_roles before advertising or invoking
// it, so descriptor metadata cannot widen the plugin's authorization map.
McpTools []*MCPToolDescriptor `protobuf:"bytes,38,rep,name=mcp_tools,json=mcpTools,proto3" json:"mcp_tools,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -443,6 +450,100 @@ func (x *PluginManifest) GetSitemap() bool {
return false return false
} }
func (x *PluginManifest) GetMcpTools() []*MCPToolDescriptor {
if x != nil {
return x.McpTools
}
return nil
}
// MCPToolDescriptor is one unary ConnectRPC method prepared for the host MCP
// catalogue. Procedure, service, method, documentation, and the request JSON
// schema are all derived from protobuf reflection during DESCRIBE.
type MCPToolDescriptor struct {
state protoimpl.MessageState `protogen:"open.v1"`
Procedure string `protobuf:"bytes,1,opt,name=procedure,proto3" json:"procedure,omitempty"`
ServiceFullName string `protobuf:"bytes,2,opt,name=service_full_name,json=serviceFullName,proto3" json:"service_full_name,omitempty"`
MethodName string `protobuf:"bytes,3,opt,name=method_name,json=methodName,proto3" json:"method_name,omitempty"`
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"`
Documentation string `protobuf:"bytes,5,opt,name=documentation,proto3" json:"documentation,omitempty"`
InputSchemaJson []byte `protobuf:"bytes,6,opt,name=input_schema_json,json=inputSchemaJson,proto3" json:"input_schema_json,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *MCPToolDescriptor) Reset() {
*x = MCPToolDescriptor{}
mi := &file_v1_manifest_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *MCPToolDescriptor) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MCPToolDescriptor) ProtoMessage() {}
func (x *MCPToolDescriptor) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MCPToolDescriptor.ProtoReflect.Descriptor instead.
func (*MCPToolDescriptor) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{1}
}
func (x *MCPToolDescriptor) GetProcedure() string {
if x != nil {
return x.Procedure
}
return ""
}
func (x *MCPToolDescriptor) GetServiceFullName() string {
if x != nil {
return x.ServiceFullName
}
return ""
}
func (x *MCPToolDescriptor) GetMethodName() string {
if x != nil {
return x.MethodName
}
return ""
}
func (x *MCPToolDescriptor) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *MCPToolDescriptor) GetDocumentation() string {
if x != nil {
return x.Documentation
}
return ""
}
func (x *MCPToolDescriptor) GetInputSchemaJson() []byte {
if x != nil {
return x.InputSchemaJson
}
return nil
}
// PublicRoute is one site-root path claim (see PluginManifest.public_routes). // PublicRoute is one site-root path claim (see PluginManifest.public_routes).
type PublicRoute struct { type PublicRoute struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -459,7 +560,7 @@ type PublicRoute struct {
func (x *PublicRoute) Reset() { func (x *PublicRoute) Reset() {
*x = PublicRoute{} *x = PublicRoute{}
mi := &file_v1_manifest_proto_msgTypes[1] mi := &file_v1_manifest_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -471,7 +572,7 @@ func (x *PublicRoute) String() string {
func (*PublicRoute) ProtoMessage() {} func (*PublicRoute) ProtoMessage() {}
func (x *PublicRoute) ProtoReflect() protoreflect.Message { func (x *PublicRoute) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[1] mi := &file_v1_manifest_proto_msgTypes[2]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -484,7 +585,7 @@ func (x *PublicRoute) ProtoReflect() protoreflect.Message {
// Deprecated: Use PublicRoute.ProtoReflect.Descriptor instead. // Deprecated: Use PublicRoute.ProtoReflect.Descriptor instead.
func (*PublicRoute) Descriptor() ([]byte, []int) { func (*PublicRoute) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{1} return file_v1_manifest_proto_rawDescGZIP(), []int{2}
} }
func (x *PublicRoute) GetPath() string { func (x *PublicRoute) GetPath() string {
@ -513,7 +614,7 @@ type Dependency struct {
func (x *Dependency) Reset() { func (x *Dependency) Reset() {
*x = Dependency{} *x = Dependency{}
mi := &file_v1_manifest_proto_msgTypes[2] mi := &file_v1_manifest_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -525,7 +626,7 @@ func (x *Dependency) String() string {
func (*Dependency) ProtoMessage() {} func (*Dependency) ProtoMessage() {}
func (x *Dependency) ProtoReflect() protoreflect.Message { func (x *Dependency) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[2] mi := &file_v1_manifest_proto_msgTypes[3]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -538,7 +639,7 @@ func (x *Dependency) ProtoReflect() protoreflect.Message {
// Deprecated: Use Dependency.ProtoReflect.Descriptor instead. // Deprecated: Use Dependency.ProtoReflect.Descriptor instead.
func (*Dependency) Descriptor() ([]byte, []int) { func (*Dependency) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{2} return file_v1_manifest_proto_rawDescGZIP(), []int{3}
} }
func (x *Dependency) GetPlugin() string { func (x *Dependency) GetPlugin() string {
@ -581,7 +682,7 @@ type BlockMeta struct {
func (x *BlockMeta) Reset() { func (x *BlockMeta) Reset() {
*x = BlockMeta{} *x = BlockMeta{}
mi := &file_v1_manifest_proto_msgTypes[3] mi := &file_v1_manifest_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -593,7 +694,7 @@ func (x *BlockMeta) String() string {
func (*BlockMeta) ProtoMessage() {} func (*BlockMeta) ProtoMessage() {}
func (x *BlockMeta) ProtoReflect() protoreflect.Message { func (x *BlockMeta) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[3] mi := &file_v1_manifest_proto_msgTypes[4]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -606,7 +707,7 @@ func (x *BlockMeta) ProtoReflect() protoreflect.Message {
// Deprecated: Use BlockMeta.ProtoReflect.Descriptor instead. // Deprecated: Use BlockMeta.ProtoReflect.Descriptor instead.
func (*BlockMeta) Descriptor() ([]byte, []int) { func (*BlockMeta) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{3} return file_v1_manifest_proto_rawDescGZIP(), []int{4}
} }
func (x *BlockMeta) GetKey() string { func (x *BlockMeta) GetKey() string {
@ -672,7 +773,7 @@ type BlockTemplateOverride struct {
func (x *BlockTemplateOverride) Reset() { func (x *BlockTemplateOverride) Reset() {
*x = BlockTemplateOverride{} *x = BlockTemplateOverride{}
mi := &file_v1_manifest_proto_msgTypes[4] mi := &file_v1_manifest_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -684,7 +785,7 @@ func (x *BlockTemplateOverride) String() string {
func (*BlockTemplateOverride) ProtoMessage() {} func (*BlockTemplateOverride) ProtoMessage() {}
func (x *BlockTemplateOverride) ProtoReflect() protoreflect.Message { func (x *BlockTemplateOverride) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[4] mi := &file_v1_manifest_proto_msgTypes[5]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -697,7 +798,7 @@ func (x *BlockTemplateOverride) ProtoReflect() protoreflect.Message {
// Deprecated: Use BlockTemplateOverride.ProtoReflect.Descriptor instead. // Deprecated: Use BlockTemplateOverride.ProtoReflect.Descriptor instead.
func (*BlockTemplateOverride) Descriptor() ([]byte, []int) { func (*BlockTemplateOverride) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{4} return file_v1_manifest_proto_rawDescGZIP(), []int{5}
} }
func (x *BlockTemplateOverride) GetTemplateKey() string { func (x *BlockTemplateOverride) GetTemplateKey() string {
@ -733,7 +834,7 @@ type SystemTemplateMeta struct {
func (x *SystemTemplateMeta) Reset() { func (x *SystemTemplateMeta) Reset() {
*x = SystemTemplateMeta{} *x = SystemTemplateMeta{}
mi := &file_v1_manifest_proto_msgTypes[5] mi := &file_v1_manifest_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -745,7 +846,7 @@ func (x *SystemTemplateMeta) String() string {
func (*SystemTemplateMeta) ProtoMessage() {} func (*SystemTemplateMeta) ProtoMessage() {}
func (x *SystemTemplateMeta) ProtoReflect() protoreflect.Message { func (x *SystemTemplateMeta) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[5] mi := &file_v1_manifest_proto_msgTypes[6]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -758,7 +859,7 @@ func (x *SystemTemplateMeta) ProtoReflect() protoreflect.Message {
// Deprecated: Use SystemTemplateMeta.ProtoReflect.Descriptor instead. // Deprecated: Use SystemTemplateMeta.ProtoReflect.Descriptor instead.
func (*SystemTemplateMeta) Descriptor() ([]byte, []int) { func (*SystemTemplateMeta) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{5} return file_v1_manifest_proto_rawDescGZIP(), []int{6}
} }
func (x *SystemTemplateMeta) GetKey() string { func (x *SystemTemplateMeta) GetKey() string {
@ -797,7 +898,7 @@ type PageTemplateMeta struct {
func (x *PageTemplateMeta) Reset() { func (x *PageTemplateMeta) Reset() {
*x = PageTemplateMeta{} *x = PageTemplateMeta{}
mi := &file_v1_manifest_proto_msgTypes[6] mi := &file_v1_manifest_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -809,7 +910,7 @@ func (x *PageTemplateMeta) String() string {
func (*PageTemplateMeta) ProtoMessage() {} func (*PageTemplateMeta) ProtoMessage() {}
func (x *PageTemplateMeta) ProtoReflect() protoreflect.Message { func (x *PageTemplateMeta) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[6] mi := &file_v1_manifest_proto_msgTypes[7]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -822,7 +923,7 @@ func (x *PageTemplateMeta) ProtoReflect() protoreflect.Message {
// Deprecated: Use PageTemplateMeta.ProtoReflect.Descriptor instead. // Deprecated: Use PageTemplateMeta.ProtoReflect.Descriptor instead.
func (*PageTemplateMeta) Descriptor() ([]byte, []int) { func (*PageTemplateMeta) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{6} return file_v1_manifest_proto_rawDescGZIP(), []int{7}
} }
func (x *PageTemplateMeta) GetSystemKey() string { func (x *PageTemplateMeta) GetSystemKey() string {
@ -876,7 +977,7 @@ type AdminPage struct {
func (x *AdminPage) Reset() { func (x *AdminPage) Reset() {
*x = AdminPage{} *x = AdminPage{}
mi := &file_v1_manifest_proto_msgTypes[7] mi := &file_v1_manifest_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -888,7 +989,7 @@ func (x *AdminPage) String() string {
func (*AdminPage) ProtoMessage() {} func (*AdminPage) ProtoMessage() {}
func (x *AdminPage) ProtoReflect() protoreflect.Message { func (x *AdminPage) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[7] mi := &file_v1_manifest_proto_msgTypes[8]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -901,7 +1002,7 @@ func (x *AdminPage) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdminPage.ProtoReflect.Descriptor instead. // Deprecated: Use AdminPage.ProtoReflect.Descriptor instead.
func (*AdminPage) Descriptor() ([]byte, []int) { func (*AdminPage) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{7} return file_v1_manifest_proto_rawDescGZIP(), []int{8}
} }
func (x *AdminPage) GetKey() string { func (x *AdminPage) GetKey() string {
@ -953,7 +1054,7 @@ type AiAction struct {
func (x *AiAction) Reset() { func (x *AiAction) Reset() {
*x = AiAction{} *x = AiAction{}
mi := &file_v1_manifest_proto_msgTypes[8] mi := &file_v1_manifest_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -965,7 +1066,7 @@ func (x *AiAction) String() string {
func (*AiAction) ProtoMessage() {} func (*AiAction) ProtoMessage() {}
func (x *AiAction) ProtoReflect() protoreflect.Message { func (x *AiAction) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[8] mi := &file_v1_manifest_proto_msgTypes[9]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -978,7 +1079,7 @@ func (x *AiAction) ProtoReflect() protoreflect.Message {
// Deprecated: Use AiAction.ProtoReflect.Descriptor instead. // Deprecated: Use AiAction.ProtoReflect.Descriptor instead.
func (*AiAction) Descriptor() ([]byte, []int) { func (*AiAction) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{8} return file_v1_manifest_proto_rawDescGZIP(), []int{9}
} }
func (x *AiAction) GetKey() string { func (x *AiAction) GetKey() string {
@ -1028,7 +1129,7 @@ type CssManifest struct {
func (x *CssManifest) Reset() { func (x *CssManifest) Reset() {
*x = CssManifest{} *x = CssManifest{}
mi := &file_v1_manifest_proto_msgTypes[9] mi := &file_v1_manifest_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1040,7 +1141,7 @@ func (x *CssManifest) String() string {
func (*CssManifest) ProtoMessage() {} func (*CssManifest) ProtoMessage() {}
func (x *CssManifest) ProtoReflect() protoreflect.Message { func (x *CssManifest) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[9] mi := &file_v1_manifest_proto_msgTypes[10]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1053,7 +1154,7 @@ func (x *CssManifest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CssManifest.ProtoReflect.Descriptor instead. // Deprecated: Use CssManifest.ProtoReflect.Descriptor instead.
func (*CssManifest) Descriptor() ([]byte, []int) { func (*CssManifest) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{9} return file_v1_manifest_proto_rawDescGZIP(), []int{10}
} }
func (x *CssManifest) GetNpmPackages() map[string]string { func (x *CssManifest) GetNpmPackages() map[string]string {
@ -1094,7 +1195,7 @@ type DirectoryExtensions struct {
func (x *DirectoryExtensions) Reset() { func (x *DirectoryExtensions) Reset() {
*x = DirectoryExtensions{} *x = DirectoryExtensions{}
mi := &file_v1_manifest_proto_msgTypes[10] mi := &file_v1_manifest_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1106,7 +1207,7 @@ func (x *DirectoryExtensions) String() string {
func (*DirectoryExtensions) ProtoMessage() {} func (*DirectoryExtensions) ProtoMessage() {}
func (x *DirectoryExtensions) ProtoReflect() protoreflect.Message { func (x *DirectoryExtensions) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[10] mi := &file_v1_manifest_proto_msgTypes[11]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1119,7 +1220,7 @@ func (x *DirectoryExtensions) ProtoReflect() protoreflect.Message {
// Deprecated: Use DirectoryExtensions.ProtoReflect.Descriptor instead. // Deprecated: Use DirectoryExtensions.ProtoReflect.Descriptor instead.
func (*DirectoryExtensions) Descriptor() ([]byte, []int) { func (*DirectoryExtensions) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{10} return file_v1_manifest_proto_rawDescGZIP(), []int{11}
} }
func (x *DirectoryExtensions) GetBooleanFilterFields() []string { func (x *DirectoryExtensions) GetBooleanFilterFields() []string {
@ -1168,7 +1269,7 @@ type BadgeLabel struct {
func (x *BadgeLabel) Reset() { func (x *BadgeLabel) Reset() {
*x = BadgeLabel{} *x = BadgeLabel{}
mi := &file_v1_manifest_proto_msgTypes[11] mi := &file_v1_manifest_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1180,7 +1281,7 @@ func (x *BadgeLabel) String() string {
func (*BadgeLabel) ProtoMessage() {} func (*BadgeLabel) ProtoMessage() {}
func (x *BadgeLabel) ProtoReflect() protoreflect.Message { func (x *BadgeLabel) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[11] mi := &file_v1_manifest_proto_msgTypes[12]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1193,7 +1294,7 @@ func (x *BadgeLabel) ProtoReflect() protoreflect.Message {
// Deprecated: Use BadgeLabel.ProtoReflect.Descriptor instead. // Deprecated: Use BadgeLabel.ProtoReflect.Descriptor instead.
func (*BadgeLabel) Descriptor() ([]byte, []int) { func (*BadgeLabel) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{11} return file_v1_manifest_proto_rawDescGZIP(), []int{12}
} }
func (x *BadgeLabel) GetPositive() string { func (x *BadgeLabel) GetPositive() string {
@ -1223,7 +1324,7 @@ type MasterPageDefinition struct {
func (x *MasterPageDefinition) Reset() { func (x *MasterPageDefinition) Reset() {
*x = MasterPageDefinition{} *x = MasterPageDefinition{}
mi := &file_v1_manifest_proto_msgTypes[12] mi := &file_v1_manifest_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1235,7 +1336,7 @@ func (x *MasterPageDefinition) String() string {
func (*MasterPageDefinition) ProtoMessage() {} func (*MasterPageDefinition) ProtoMessage() {}
func (x *MasterPageDefinition) ProtoReflect() protoreflect.Message { func (x *MasterPageDefinition) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[12] mi := &file_v1_manifest_proto_msgTypes[13]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1248,7 +1349,7 @@ func (x *MasterPageDefinition) ProtoReflect() protoreflect.Message {
// Deprecated: Use MasterPageDefinition.ProtoReflect.Descriptor instead. // Deprecated: Use MasterPageDefinition.ProtoReflect.Descriptor instead.
func (*MasterPageDefinition) Descriptor() ([]byte, []int) { func (*MasterPageDefinition) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{12} return file_v1_manifest_proto_rawDescGZIP(), []int{13}
} }
func (x *MasterPageDefinition) GetKey() string { func (x *MasterPageDefinition) GetKey() string {
@ -1295,7 +1396,7 @@ type MasterPageBlock struct {
func (x *MasterPageBlock) Reset() { func (x *MasterPageBlock) Reset() {
*x = MasterPageBlock{} *x = MasterPageBlock{}
mi := &file_v1_manifest_proto_msgTypes[13] mi := &file_v1_manifest_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1307,7 +1408,7 @@ func (x *MasterPageBlock) String() string {
func (*MasterPageBlock) ProtoMessage() {} func (*MasterPageBlock) ProtoMessage() {}
func (x *MasterPageBlock) ProtoReflect() protoreflect.Message { func (x *MasterPageBlock) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[13] mi := &file_v1_manifest_proto_msgTypes[14]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1320,7 +1421,7 @@ func (x *MasterPageBlock) ProtoReflect() protoreflect.Message {
// Deprecated: Use MasterPageBlock.ProtoReflect.Descriptor instead. // Deprecated: Use MasterPageBlock.ProtoReflect.Descriptor instead.
func (*MasterPageBlock) Descriptor() ([]byte, []int) { func (*MasterPageBlock) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{13} return file_v1_manifest_proto_rawDescGZIP(), []int{14}
} }
func (x *MasterPageBlock) GetBlockKey() string { func (x *MasterPageBlock) GetBlockKey() string {
@ -1377,7 +1478,7 @@ type CoreServiceBinding struct {
func (x *CoreServiceBinding) Reset() { func (x *CoreServiceBinding) Reset() {
*x = CoreServiceBinding{} *x = CoreServiceBinding{}
mi := &file_v1_manifest_proto_msgTypes[14] mi := &file_v1_manifest_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1389,7 +1490,7 @@ func (x *CoreServiceBinding) String() string {
func (*CoreServiceBinding) ProtoMessage() {} func (*CoreServiceBinding) ProtoMessage() {}
func (x *CoreServiceBinding) ProtoReflect() protoreflect.Message { func (x *CoreServiceBinding) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[14] mi := &file_v1_manifest_proto_msgTypes[15]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1402,7 +1503,7 @@ func (x *CoreServiceBinding) ProtoReflect() protoreflect.Message {
// Deprecated: Use CoreServiceBinding.ProtoReflect.Descriptor instead. // Deprecated: Use CoreServiceBinding.ProtoReflect.Descriptor instead.
func (*CoreServiceBinding) Descriptor() ([]byte, []int) { func (*CoreServiceBinding) Descriptor() ([]byte, []int) {
return file_v1_manifest_proto_rawDescGZIP(), []int{14} return file_v1_manifest_proto_rawDescGZIP(), []int{15}
} }
func (x *CoreServiceBinding) GetServiceName() string { func (x *CoreServiceBinding) GetServiceName() string {
@ -1423,7 +1524,7 @@ var File_v1_manifest_proto protoreflect.FileDescriptor
const file_v1_manifest_proto_rawDesc = "" + const file_v1_manifest_proto_rawDesc = "" +
"\n" + "\n" +
"\x11v1/manifest.proto\x12\x06abi.v1\"\xaf\x0e\n" + "\x11v1/manifest.proto\x12\x06abi.v1\"\xe7\x0e\n" +
"\x0ePluginManifest\x12\x1f\n" + "\x0ePluginManifest\x12\x1f\n" +
"\vabi_version\x18\x01 \x01(\rR\n" + "\vabi_version\x18\x01 \x01(\rR\n" +
"abiVersion\x12\x12\n" + "abiVersion\x12\x12\n" +
@ -1465,10 +1566,19 @@ const file_v1_manifest_proto_rawDesc = "" +
"\rallowed_hosts\x18\" \x03(\tR\fallowedHosts\x12&\n" + "\rallowed_hosts\x18\" \x03(\tR\fallowedHosts\x12&\n" +
"\x0fmax_response_mb\x18# \x01(\rR\rmaxResponseMb\x128\n" + "\x0fmax_response_mb\x18# \x01(\rR\rmaxResponseMb\x128\n" +
"\rpublic_routes\x18$ \x03(\v2\x13.abi.v1.PublicRouteR\fpublicRoutes\x12\x18\n" + "\rpublic_routes\x18$ \x03(\v2\x13.abi.v1.PublicRouteR\fpublicRoutes\x12\x18\n" +
"\asitemap\x18% \x01(\bR\asitemap\x1aB\n" + "\asitemap\x18% \x01(\bR\asitemap\x126\n" +
"\tmcp_tools\x18& \x03(\v2\x19.abi.v1.MCPToolDescriptorR\bmcpTools\x1aB\n" +
"\x14RbacMethodRolesEntry\x12\x10\n" + "\x14RbacMethodRolesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"9\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xf2\x01\n" +
"\x11MCPToolDescriptor\x12\x1c\n" +
"\tprocedure\x18\x01 \x01(\tR\tprocedure\x12*\n" +
"\x11service_full_name\x18\x02 \x01(\tR\x0fserviceFullName\x12\x1f\n" +
"\vmethod_name\x18\x03 \x01(\tR\n" +
"methodName\x12 \n" +
"\vdescription\x18\x04 \x01(\tR\vdescription\x12$\n" +
"\rdocumentation\x18\x05 \x01(\tR\rdocumentation\x12*\n" +
"\x11input_schema_json\x18\x06 \x01(\fR\x0finputSchemaJson\"9\n" +
"\vPublicRoute\x12\x12\n" + "\vPublicRoute\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x16\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x16\n" +
"\x06prefix\x18\x02 \x01(\bR\x06prefix\"a\n" + "\x06prefix\x18\x02 \x01(\bR\x06prefix\"a\n" +
@ -1566,52 +1676,54 @@ func file_v1_manifest_proto_rawDescGZIP() []byte {
return file_v1_manifest_proto_rawDescData return file_v1_manifest_proto_rawDescData
} }
var file_v1_manifest_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_v1_manifest_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
var file_v1_manifest_proto_goTypes = []any{ var file_v1_manifest_proto_goTypes = []any{
(*PluginManifest)(nil), // 0: abi.v1.PluginManifest (*PluginManifest)(nil), // 0: abi.v1.PluginManifest
(*PublicRoute)(nil), // 1: abi.v1.PublicRoute (*MCPToolDescriptor)(nil), // 1: abi.v1.MCPToolDescriptor
(*Dependency)(nil), // 2: abi.v1.Dependency (*PublicRoute)(nil), // 2: abi.v1.PublicRoute
(*BlockMeta)(nil), // 3: abi.v1.BlockMeta (*Dependency)(nil), // 3: abi.v1.Dependency
(*BlockTemplateOverride)(nil), // 4: abi.v1.BlockTemplateOverride (*BlockMeta)(nil), // 4: abi.v1.BlockMeta
(*SystemTemplateMeta)(nil), // 5: abi.v1.SystemTemplateMeta (*BlockTemplateOverride)(nil), // 5: abi.v1.BlockTemplateOverride
(*PageTemplateMeta)(nil), // 6: abi.v1.PageTemplateMeta (*SystemTemplateMeta)(nil), // 6: abi.v1.SystemTemplateMeta
(*AdminPage)(nil), // 7: abi.v1.AdminPage (*PageTemplateMeta)(nil), // 7: abi.v1.PageTemplateMeta
(*AiAction)(nil), // 8: abi.v1.AiAction (*AdminPage)(nil), // 8: abi.v1.AdminPage
(*CssManifest)(nil), // 9: abi.v1.CssManifest (*AiAction)(nil), // 9: abi.v1.AiAction
(*DirectoryExtensions)(nil), // 10: abi.v1.DirectoryExtensions (*CssManifest)(nil), // 10: abi.v1.CssManifest
(*BadgeLabel)(nil), // 11: abi.v1.BadgeLabel (*DirectoryExtensions)(nil), // 11: abi.v1.DirectoryExtensions
(*MasterPageDefinition)(nil), // 12: abi.v1.MasterPageDefinition (*BadgeLabel)(nil), // 12: abi.v1.BadgeLabel
(*MasterPageBlock)(nil), // 13: abi.v1.MasterPageBlock (*MasterPageDefinition)(nil), // 13: abi.v1.MasterPageDefinition
(*CoreServiceBinding)(nil), // 14: abi.v1.CoreServiceBinding (*MasterPageBlock)(nil), // 14: abi.v1.MasterPageBlock
nil, // 15: abi.v1.PluginManifest.RbacMethodRolesEntry (*CoreServiceBinding)(nil), // 15: abi.v1.CoreServiceBinding
nil, // 16: abi.v1.CssManifest.NpmPackagesEntry nil, // 16: abi.v1.PluginManifest.RbacMethodRolesEntry
nil, // 17: abi.v1.DirectoryExtensions.BadgeLabelsEntry nil, // 17: abi.v1.CssManifest.NpmPackagesEntry
nil, // 18: abi.v1.CoreServiceBinding.MethodRolesEntry nil, // 18: abi.v1.DirectoryExtensions.BadgeLabelsEntry
nil, // 19: abi.v1.CoreServiceBinding.MethodRolesEntry
} }
var file_v1_manifest_proto_depIdxs = []int32{ var file_v1_manifest_proto_depIdxs = []int32{
2, // 0: abi.v1.PluginManifest.dependencies:type_name -> abi.v1.Dependency 3, // 0: abi.v1.PluginManifest.dependencies:type_name -> abi.v1.Dependency
3, // 1: abi.v1.PluginManifest.blocks:type_name -> abi.v1.BlockMeta 4, // 1: abi.v1.PluginManifest.blocks:type_name -> abi.v1.BlockMeta
4, // 2: abi.v1.PluginManifest.block_template_overrides:type_name -> abi.v1.BlockTemplateOverride 5, // 2: abi.v1.PluginManifest.block_template_overrides:type_name -> abi.v1.BlockTemplateOverride
5, // 3: abi.v1.PluginManifest.system_templates:type_name -> abi.v1.SystemTemplateMeta 6, // 3: abi.v1.PluginManifest.system_templates:type_name -> abi.v1.SystemTemplateMeta
6, // 4: abi.v1.PluginManifest.page_templates:type_name -> abi.v1.PageTemplateMeta 7, // 4: abi.v1.PluginManifest.page_templates:type_name -> abi.v1.PageTemplateMeta
7, // 5: abi.v1.PluginManifest.admin_pages:type_name -> abi.v1.AdminPage 8, // 5: abi.v1.PluginManifest.admin_pages:type_name -> abi.v1.AdminPage
12, // 6: abi.v1.PluginManifest.master_pages:type_name -> abi.v1.MasterPageDefinition 13, // 6: abi.v1.PluginManifest.master_pages:type_name -> abi.v1.MasterPageDefinition
8, // 7: abi.v1.PluginManifest.ai_actions:type_name -> abi.v1.AiAction 9, // 7: abi.v1.PluginManifest.ai_actions:type_name -> abi.v1.AiAction
15, // 8: abi.v1.PluginManifest.rbac_method_roles:type_name -> abi.v1.PluginManifest.RbacMethodRolesEntry 16, // 8: abi.v1.PluginManifest.rbac_method_roles:type_name -> abi.v1.PluginManifest.RbacMethodRolesEntry
9, // 9: abi.v1.PluginManifest.css_manifest:type_name -> abi.v1.CssManifest 10, // 9: abi.v1.PluginManifest.css_manifest:type_name -> abi.v1.CssManifest
10, // 10: abi.v1.PluginManifest.directory_extensions:type_name -> abi.v1.DirectoryExtensions 11, // 10: abi.v1.PluginManifest.directory_extensions:type_name -> abi.v1.DirectoryExtensions
14, // 11: abi.v1.PluginManifest.core_service_bindings:type_name -> abi.v1.CoreServiceBinding 15, // 11: abi.v1.PluginManifest.core_service_bindings:type_name -> abi.v1.CoreServiceBinding
1, // 12: abi.v1.PluginManifest.public_routes:type_name -> abi.v1.PublicRoute 2, // 12: abi.v1.PluginManifest.public_routes:type_name -> abi.v1.PublicRoute
16, // 13: abi.v1.CssManifest.npm_packages:type_name -> abi.v1.CssManifest.NpmPackagesEntry 1, // 13: abi.v1.PluginManifest.mcp_tools:type_name -> abi.v1.MCPToolDescriptor
17, // 14: abi.v1.DirectoryExtensions.badge_labels:type_name -> abi.v1.DirectoryExtensions.BadgeLabelsEntry 17, // 14: abi.v1.CssManifest.npm_packages:type_name -> abi.v1.CssManifest.NpmPackagesEntry
13, // 15: abi.v1.MasterPageDefinition.blocks:type_name -> abi.v1.MasterPageBlock 18, // 15: abi.v1.DirectoryExtensions.badge_labels:type_name -> abi.v1.DirectoryExtensions.BadgeLabelsEntry
18, // 16: abi.v1.CoreServiceBinding.method_roles:type_name -> abi.v1.CoreServiceBinding.MethodRolesEntry 14, // 16: abi.v1.MasterPageDefinition.blocks:type_name -> abi.v1.MasterPageBlock
11, // 17: abi.v1.DirectoryExtensions.BadgeLabelsEntry.value:type_name -> abi.v1.BadgeLabel 19, // 17: abi.v1.CoreServiceBinding.method_roles:type_name -> abi.v1.CoreServiceBinding.MethodRolesEntry
18, // [18:18] is the sub-list for method output_type 12, // 18: abi.v1.DirectoryExtensions.BadgeLabelsEntry.value:type_name -> abi.v1.BadgeLabel
18, // [18:18] is the sub-list for method input_type 19, // [19:19] is the sub-list for method output_type
18, // [18:18] is the sub-list for extension type_name 19, // [19:19] is the sub-list for method input_type
18, // [18:18] is the sub-list for extension extendee 19, // [19:19] is the sub-list for extension type_name
0, // [0:18] is the sub-list for field type_name 19, // [19:19] is the sub-list for extension extendee
0, // [0:19] is the sub-list for field type_name
} }
func init() { file_v1_manifest_proto_init() } func init() { file_v1_manifest_proto_init() }
@ -1619,14 +1731,14 @@ func file_v1_manifest_proto_init() {
if File_v1_manifest_proto != nil { if File_v1_manifest_proto != nil {
return return
} }
file_v1_manifest_proto_msgTypes[13].OneofWrappers = []any{} file_v1_manifest_proto_msgTypes[14].OneofWrappers = []any{}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_manifest_proto_rawDesc), len(file_v1_manifest_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_manifest_proto_rawDesc), len(file_v1_manifest_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 19, NumMessages: 20,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@ -247,12 +247,14 @@ func (g *guest) captureServiceRegistration(m *abiv1.PluginManifest) {
if coreBound[svc.Name()] { if coreBound[svc.Name()] {
continue // declared via core_service_bindings, host mounts it continue // declared via core_service_bindings, host mounts it
} }
for method, role := range svc.MethodRoles() { methodRoles := svc.MethodRoles()
for method, role := range methodRoles {
if m.RbacMethodRoles == nil { if m.RbacMethodRoles == nil {
m.RbacMethodRoles = make(map[string]string) m.RbacMethodRoles = make(map[string]string)
} }
m.RbacMethodRoles[method] = string(role) m.RbacMethodRoles[method] = string(role)
} }
m.McpTools = append(m.McpTools, captureMCPTools(methodRoles)...)
} }
} }

206
plugin/wasmguest/mcp.go Normal file
View File

@ -0,0 +1,206 @@
package wasmguest
import (
"encoding/json"
"sort"
"strings"
"unicode"
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/rbac"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
// captureMCPTools resolves the plugin's registered Connect procedures through
// the protobuf registry compiled into the guest. It emits only unary methods
// that have an explicit RBAC mapping; the host repeats that RBAC intersection
// before adding the definitions to its live MCP catalogue.
func captureMCPTools(methodRoles map[string]rbac.Role) []*abiv1.MCPToolDescriptor {
procedures := make([]string, 0, len(methodRoles))
for procedure := range methodRoles {
procedures = append(procedures, procedure)
}
sort.Strings(procedures)
tools := make([]*abiv1.MCPToolDescriptor, 0, len(procedures))
for _, procedure := range procedures {
serviceName, methodName, ok := splitConnectProcedure(procedure)
if !ok {
continue
}
desc, err := protoregistry.GlobalFiles.FindDescriptorByName(protoreflect.FullName(serviceName))
if err != nil {
continue
}
service, ok := desc.(protoreflect.ServiceDescriptor)
if !ok {
continue
}
method := service.Methods().ByName(protoreflect.Name(methodName))
if method == nil || method.IsStreamingClient() || method.IsStreamingServer() {
continue
}
schema, err := json.Marshal(schemaForMessage(method.Input(), map[protoreflect.FullName]bool{}))
if err != nil {
continue
}
description, documentation := methodDocumentation(method)
tools = append(tools, &abiv1.MCPToolDescriptor{
Procedure: procedure,
ServiceFullName: serviceName,
MethodName: methodName,
Description: description,
Documentation: documentation,
InputSchemaJson: schema,
})
}
return tools
}
func splitConnectProcedure(procedure string) (serviceName, methodName string, ok bool) {
trimmed := strings.TrimPrefix(strings.TrimSpace(procedure), "/")
serviceName, methodName, ok = strings.Cut(trimmed, "/")
return serviceName, methodName, ok && serviceName != "" && methodName != "" && !strings.Contains(methodName, "/")
}
func methodDocumentation(method protoreflect.MethodDescriptor) (description, documentation string) {
location := method.ParentFile().SourceLocations().ByDescriptor(method)
documentation = strings.TrimSpace(location.LeadingComments)
paragraph := documentation
if before, _, ok := strings.Cut(paragraph, "\n\n"); ok {
paragraph = before
}
paragraph = strings.TrimSpace(strings.ReplaceAll(paragraph, "\n", " "))
methodName := string(method.Name())
if strings.HasPrefix(paragraph, methodName+" ") {
paragraph = strings.TrimSpace(strings.TrimPrefix(paragraph, methodName+" "))
if paragraph != "" {
runes := []rune(paragraph)
runes[0] = unicode.ToUpper(runes[0])
paragraph = string(runes)
}
}
if paragraph == "" {
paragraph = "Calls " + string(method.FullName()) + "."
}
return paragraph, documentation
}
func schemaForMessage(message protoreflect.MessageDescriptor, visited map[protoreflect.FullName]bool) map[string]any {
object := map[string]any{"type": "object"}
if message == nil || visited[message.FullName()] {
return object
}
visited[message.FullName()] = true
defer delete(visited, message.FullName())
properties := make(map[string]any, message.Fields().Len())
for i := range message.Fields().Len() {
field := message.Fields().Get(i)
properties[string(field.Name())] = fieldSchema(field, visited)
}
if len(properties) > 0 {
object["properties"] = properties
}
return object
}
func fieldSchema(field protoreflect.FieldDescriptor, visited map[protoreflect.FullName]bool) map[string]any {
var schema map[string]any
switch {
case field.IsMap():
schema = map[string]any{
"type": "object",
"additionalProperties": singularFieldSchema(field.MapValue(), visited),
}
case field.IsList():
schema = map[string]any{"type": "array", "items": singularFieldSchema(field, visited)}
default:
schema = singularFieldSchema(field, visited)
}
location := field.ParentFile().SourceLocations().ByDescriptor(field)
if comment := strings.TrimSpace(strings.ReplaceAll(location.LeadingComments, "\n", " ")); comment != "" {
schema["description"] = comment
}
return schema
}
func singularFieldSchema(field protoreflect.FieldDescriptor, visited map[protoreflect.FullName]bool) map[string]any {
switch field.Kind() {
case protoreflect.MessageKind, protoreflect.GroupKind:
return wellKnownOrMessage(field.Message(), visited)
case protoreflect.EnumKind:
return enumSchema(field.Enum())
default:
return scalarSchema(field.Kind())
}
}
func scalarSchema(kind protoreflect.Kind) map[string]any {
switch kind {
case protoreflect.BoolKind:
return map[string]any{"type": "boolean"}
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind,
protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
return map[string]any{"type": "integer"}
case protoreflect.FloatKind, protoreflect.DoubleKind:
return map[string]any{"type": "number"}
case protoreflect.StringKind:
return map[string]any{"type": "string"}
case protoreflect.BytesKind:
return map[string]any{"type": "string", "contentEncoding": "base64"}
default:
return map[string]any{}
}
}
func enumSchema(enum protoreflect.EnumDescriptor) map[string]any {
schema := map[string]any{"type": "string"}
if enum == nil {
return schema
}
values := make([]any, 0, enum.Values().Len())
for i := range enum.Values().Len() {
values = append(values, string(enum.Values().Get(i).Name()))
}
if len(values) > 0 {
schema["enum"] = values
}
return schema
}
func wellKnownOrMessage(message protoreflect.MessageDescriptor, visited map[protoreflect.FullName]bool) map[string]any {
if message == nil {
return map[string]any{"type": "object"}
}
switch message.FullName() {
case "google.protobuf.Timestamp":
return map[string]any{"type": "string", "format": "date-time"}
case "google.protobuf.Duration", "google.protobuf.FieldMask":
return map[string]any{"type": "string"}
case "google.protobuf.Struct":
return map[string]any{"type": "object"}
case "google.protobuf.ListValue":
return map[string]any{"type": "array"}
case "google.protobuf.Value", "google.protobuf.Any":
return map[string]any{}
case "google.protobuf.Empty":
return map[string]any{"type": "object"}
case "google.protobuf.StringValue", "google.protobuf.BytesValue":
return map[string]any{"type": "string"}
case "google.protobuf.BoolValue":
return map[string]any{"type": "boolean"}
case "google.protobuf.Int32Value", "google.protobuf.UInt32Value",
"google.protobuf.Int64Value", "google.protobuf.UInt64Value":
return map[string]any{"type": "integer"}
case "google.protobuf.FloatValue", "google.protobuf.DoubleValue":
return map[string]any{"type": "number"}
default:
return schemaForMessage(message, visited)
}
}

View File

@ -0,0 +1,100 @@
package wasmguest
import (
"encoding/json"
"testing"
"git.dev.alexdunmow.com/block/pluginsdk/rbac"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
_ "google.golang.org/protobuf/types/known/timestamppb"
)
func TestCaptureMCPToolsUsesCompiledProtobufDescriptor(t *testing.T) {
registerMCPFixture(t)
procedure := "/mcpfixture.v1.WikiService/ListArticles"
tools := captureMCPTools(map[string]rbac.Role{procedure: rbac.RoleAdmin})
if len(tools) != 1 {
t.Fatalf("captureMCPTools() returned %d tools, want 1", len(tools))
}
tool := tools[0]
if tool.GetProcedure() != procedure || tool.GetServiceFullName() != "mcpfixture.v1.WikiService" || tool.GetMethodName() != "ListArticles" {
t.Fatalf("unexpected procedure metadata: %#v", tool)
}
if tool.GetDescription() != "Lists Wiki articles." || tool.GetDocumentation() != "ListArticles lists Wiki articles.\n\nSupports pagination." {
t.Fatalf("unexpected descriptor documentation: description=%q doc=%q", tool.GetDescription(), tool.GetDocumentation())
}
var schema map[string]any
if err := json.Unmarshal(tool.GetInputSchemaJson(), &schema); err != nil {
t.Fatalf("schema is not JSON: %v", err)
}
properties, ok := schema["properties"].(map[string]any)
if !ok {
t.Fatalf("schema properties = %#v", schema["properties"])
}
assertSchemaType(t, properties, "search", "string")
assertSchemaType(t, properties, "page", "integer")
assertSchemaType(t, properties, "published_after", "string")
}
func TestCaptureMCPToolsSkipsUnresolvableAndStreamingProcedures(t *testing.T) {
registerMCPFixture(t)
tools := captureMCPTools(map[string]rbac.Role{
"/mcpfixture.v1.WikiService/StreamArticles": rbac.RoleAdmin,
"/missing.v1.WikiService/ListArticles": rbac.RoleAdmin,
"not-a-procedure": rbac.RoleAdmin,
})
if len(tools) != 0 {
t.Fatalf("captureMCPTools() returned %d tools, want fail-closed omission", len(tools))
}
}
func assertSchemaType(t *testing.T, properties map[string]any, field, expected string) {
t.Helper()
property, ok := properties[field].(map[string]any)
if !ok || property["type"] != expected {
t.Fatalf("schema field %q = %#v, want type %q", field, properties[field], expected)
}
}
func registerMCPFixture(t *testing.T) {
t.Helper()
if _, err := protoregistry.GlobalFiles.FindFileByPath("test/mcpfixture.proto"); err == nil {
return
}
file, err := protodesc.NewFile(&descriptorpb.FileDescriptorProto{
Name: proto.String("test/mcpfixture.proto"),
Package: proto.String("mcpfixture.v1"),
Syntax: proto.String("proto3"),
Dependency: []string{"google/protobuf/timestamp.proto"},
MessageType: []*descriptorpb.DescriptorProto{{
Name: proto.String("ListArticlesRequest"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("search"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
{Name: proto.String("page"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum()},
{Name: proto.String("published_after"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: proto.String(".google.protobuf.Timestamp")},
},
}, {
Name: proto.String("ListArticlesResponse"),
}},
Service: []*descriptorpb.ServiceDescriptorProto{{
Name: proto.String("WikiService"),
Method: []*descriptorpb.MethodDescriptorProto{{
Name: proto.String("ListArticles"), InputType: proto.String(".mcpfixture.v1.ListArticlesRequest"), OutputType: proto.String(".mcpfixture.v1.ListArticlesResponse"),
}, {
Name: proto.String("StreamArticles"), InputType: proto.String(".mcpfixture.v1.ListArticlesRequest"), OutputType: proto.String(".mcpfixture.v1.ListArticlesResponse"), ServerStreaming: proto.Bool(true),
}},
}},
SourceCodeInfo: &descriptorpb.SourceCodeInfo{Location: []*descriptorpb.SourceCodeInfo_Location{{
Path: []int32{6, 0, 2, 0}, LeadingComments: proto.String("ListArticles lists Wiki articles.\n\nSupports pagination."),
}}},
}, protoregistry.GlobalFiles)
if err != nil {
t.Fatalf("create fixture descriptor: %v", err)
}
if err := protoregistry.GlobalFiles.RegisterFile(file); err != nil {
t.Fatalf("register fixture descriptor: %v", err)
}
}