Compare commits

..

No commits in common. "1ee4db6f0d315e6b914425409a0312a9a04b4cd8" and "bf72cd160ab3d10c8b2d7ab0e3086d97cf441279" have entirely different histories.

6 changed files with 91 additions and 562 deletions

View File

@ -156,26 +156,6 @@ 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,13 +150,6 @@ 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
} }
@ -450,100 +443,6 @@ 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"`
@ -560,7 +459,7 @@ type PublicRoute struct {
func (x *PublicRoute) Reset() { func (x *PublicRoute) Reset() {
*x = PublicRoute{} *x = PublicRoute{}
mi := &file_v1_manifest_proto_msgTypes[2] mi := &file_v1_manifest_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -572,7 +471,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[2] mi := &file_v1_manifest_proto_msgTypes[1]
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 {
@ -585,7 +484,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{2} return file_v1_manifest_proto_rawDescGZIP(), []int{1}
} }
func (x *PublicRoute) GetPath() string { func (x *PublicRoute) GetPath() string {
@ -614,7 +513,7 @@ type Dependency struct {
func (x *Dependency) Reset() { func (x *Dependency) Reset() {
*x = Dependency{} *x = Dependency{}
mi := &file_v1_manifest_proto_msgTypes[3] 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)
} }
@ -626,7 +525,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[3] 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 {
@ -639,7 +538,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{3} return file_v1_manifest_proto_rawDescGZIP(), []int{2}
} }
func (x *Dependency) GetPlugin() string { func (x *Dependency) GetPlugin() string {
@ -682,7 +581,7 @@ type BlockMeta struct {
func (x *BlockMeta) Reset() { func (x *BlockMeta) Reset() {
*x = BlockMeta{} *x = BlockMeta{}
mi := &file_v1_manifest_proto_msgTypes[4] 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)
} }
@ -694,7 +593,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[4] 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 {
@ -707,7 +606,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{4} return file_v1_manifest_proto_rawDescGZIP(), []int{3}
} }
func (x *BlockMeta) GetKey() string { func (x *BlockMeta) GetKey() string {
@ -773,7 +672,7 @@ type BlockTemplateOverride struct {
func (x *BlockTemplateOverride) Reset() { func (x *BlockTemplateOverride) Reset() {
*x = BlockTemplateOverride{} *x = BlockTemplateOverride{}
mi := &file_v1_manifest_proto_msgTypes[5] 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)
} }
@ -785,7 +684,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[5] 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 {
@ -798,7 +697,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{5} return file_v1_manifest_proto_rawDescGZIP(), []int{4}
} }
func (x *BlockTemplateOverride) GetTemplateKey() string { func (x *BlockTemplateOverride) GetTemplateKey() string {
@ -834,7 +733,7 @@ type SystemTemplateMeta struct {
func (x *SystemTemplateMeta) Reset() { func (x *SystemTemplateMeta) Reset() {
*x = SystemTemplateMeta{} *x = SystemTemplateMeta{}
mi := &file_v1_manifest_proto_msgTypes[6] 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)
} }
@ -846,7 +745,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[6] 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 {
@ -859,7 +758,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{6} return file_v1_manifest_proto_rawDescGZIP(), []int{5}
} }
func (x *SystemTemplateMeta) GetKey() string { func (x *SystemTemplateMeta) GetKey() string {
@ -898,7 +797,7 @@ type PageTemplateMeta struct {
func (x *PageTemplateMeta) Reset() { func (x *PageTemplateMeta) Reset() {
*x = PageTemplateMeta{} *x = PageTemplateMeta{}
mi := &file_v1_manifest_proto_msgTypes[7] 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)
} }
@ -910,7 +809,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[7] 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 {
@ -923,7 +822,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{7} return file_v1_manifest_proto_rawDescGZIP(), []int{6}
} }
func (x *PageTemplateMeta) GetSystemKey() string { func (x *PageTemplateMeta) GetSystemKey() string {
@ -977,7 +876,7 @@ type AdminPage struct {
func (x *AdminPage) Reset() { func (x *AdminPage) Reset() {
*x = AdminPage{} *x = AdminPage{}
mi := &file_v1_manifest_proto_msgTypes[8] 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)
} }
@ -989,7 +888,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[8] 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 {
@ -1002,7 +901,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{8} return file_v1_manifest_proto_rawDescGZIP(), []int{7}
} }
func (x *AdminPage) GetKey() string { func (x *AdminPage) GetKey() string {
@ -1054,7 +953,7 @@ type AiAction struct {
func (x *AiAction) Reset() { func (x *AiAction) Reset() {
*x = AiAction{} *x = AiAction{}
mi := &file_v1_manifest_proto_msgTypes[9] 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)
} }
@ -1066,7 +965,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[9] 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 {
@ -1079,7 +978,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{9} return file_v1_manifest_proto_rawDescGZIP(), []int{8}
} }
func (x *AiAction) GetKey() string { func (x *AiAction) GetKey() string {
@ -1129,7 +1028,7 @@ type CssManifest struct {
func (x *CssManifest) Reset() { func (x *CssManifest) Reset() {
*x = CssManifest{} *x = CssManifest{}
mi := &file_v1_manifest_proto_msgTypes[10] 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)
} }
@ -1141,7 +1040,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[10] 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 {
@ -1154,7 +1053,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{10} return file_v1_manifest_proto_rawDescGZIP(), []int{9}
} }
func (x *CssManifest) GetNpmPackages() map[string]string { func (x *CssManifest) GetNpmPackages() map[string]string {
@ -1195,7 +1094,7 @@ type DirectoryExtensions struct {
func (x *DirectoryExtensions) Reset() { func (x *DirectoryExtensions) Reset() {
*x = DirectoryExtensions{} *x = DirectoryExtensions{}
mi := &file_v1_manifest_proto_msgTypes[11] 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)
} }
@ -1207,7 +1106,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[11] 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 {
@ -1220,7 +1119,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{11} return file_v1_manifest_proto_rawDescGZIP(), []int{10}
} }
func (x *DirectoryExtensions) GetBooleanFilterFields() []string { func (x *DirectoryExtensions) GetBooleanFilterFields() []string {
@ -1269,7 +1168,7 @@ type BadgeLabel struct {
func (x *BadgeLabel) Reset() { func (x *BadgeLabel) Reset() {
*x = BadgeLabel{} *x = BadgeLabel{}
mi := &file_v1_manifest_proto_msgTypes[12] 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)
} }
@ -1281,7 +1180,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[12] 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 {
@ -1294,7 +1193,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{12} return file_v1_manifest_proto_rawDescGZIP(), []int{11}
} }
func (x *BadgeLabel) GetPositive() string { func (x *BadgeLabel) GetPositive() string {
@ -1324,7 +1223,7 @@ type MasterPageDefinition struct {
func (x *MasterPageDefinition) Reset() { func (x *MasterPageDefinition) Reset() {
*x = MasterPageDefinition{} *x = MasterPageDefinition{}
mi := &file_v1_manifest_proto_msgTypes[13] 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)
} }
@ -1336,7 +1235,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[13] 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 {
@ -1349,7 +1248,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{13} return file_v1_manifest_proto_rawDescGZIP(), []int{12}
} }
func (x *MasterPageDefinition) GetKey() string { func (x *MasterPageDefinition) GetKey() string {
@ -1396,7 +1295,7 @@ type MasterPageBlock struct {
func (x *MasterPageBlock) Reset() { func (x *MasterPageBlock) Reset() {
*x = MasterPageBlock{} *x = MasterPageBlock{}
mi := &file_v1_manifest_proto_msgTypes[14] 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)
} }
@ -1408,7 +1307,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[14] 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 {
@ -1421,7 +1320,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{14} return file_v1_manifest_proto_rawDescGZIP(), []int{13}
} }
func (x *MasterPageBlock) GetBlockKey() string { func (x *MasterPageBlock) GetBlockKey() string {
@ -1478,7 +1377,7 @@ type CoreServiceBinding struct {
func (x *CoreServiceBinding) Reset() { func (x *CoreServiceBinding) Reset() {
*x = CoreServiceBinding{} *x = CoreServiceBinding{}
mi := &file_v1_manifest_proto_msgTypes[15] 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)
} }
@ -1490,7 +1389,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[15] 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 {
@ -1503,7 +1402,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{15} return file_v1_manifest_proto_rawDescGZIP(), []int{14}
} }
func (x *CoreServiceBinding) GetServiceName() string { func (x *CoreServiceBinding) GetServiceName() string {
@ -1524,7 +1423,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\"\xe7\x0e\n" + "\x11v1/manifest.proto\x12\x06abi.v1\"\xaf\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" +
@ -1566,19 +1465,10 @@ 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\x126\n" + "\asitemap\x18% \x01(\bR\asitemap\x1aB\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\"\xf2\x01\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"9\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" +
@ -1676,54 +1566,52 @@ 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, 20) var file_v1_manifest_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
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
(*MCPToolDescriptor)(nil), // 1: abi.v1.MCPToolDescriptor (*PublicRoute)(nil), // 1: abi.v1.PublicRoute
(*PublicRoute)(nil), // 2: abi.v1.PublicRoute (*Dependency)(nil), // 2: abi.v1.Dependency
(*Dependency)(nil), // 3: abi.v1.Dependency (*BlockMeta)(nil), // 3: abi.v1.BlockMeta
(*BlockMeta)(nil), // 4: abi.v1.BlockMeta (*BlockTemplateOverride)(nil), // 4: abi.v1.BlockTemplateOverride
(*BlockTemplateOverride)(nil), // 5: abi.v1.BlockTemplateOverride (*SystemTemplateMeta)(nil), // 5: abi.v1.SystemTemplateMeta
(*SystemTemplateMeta)(nil), // 6: abi.v1.SystemTemplateMeta (*PageTemplateMeta)(nil), // 6: abi.v1.PageTemplateMeta
(*PageTemplateMeta)(nil), // 7: abi.v1.PageTemplateMeta (*AdminPage)(nil), // 7: abi.v1.AdminPage
(*AdminPage)(nil), // 8: abi.v1.AdminPage (*AiAction)(nil), // 8: abi.v1.AiAction
(*AiAction)(nil), // 9: abi.v1.AiAction (*CssManifest)(nil), // 9: abi.v1.CssManifest
(*CssManifest)(nil), // 10: abi.v1.CssManifest (*DirectoryExtensions)(nil), // 10: abi.v1.DirectoryExtensions
(*DirectoryExtensions)(nil), // 11: abi.v1.DirectoryExtensions (*BadgeLabel)(nil), // 11: abi.v1.BadgeLabel
(*BadgeLabel)(nil), // 12: abi.v1.BadgeLabel (*MasterPageDefinition)(nil), // 12: abi.v1.MasterPageDefinition
(*MasterPageDefinition)(nil), // 13: abi.v1.MasterPageDefinition (*MasterPageBlock)(nil), // 13: abi.v1.MasterPageBlock
(*MasterPageBlock)(nil), // 14: abi.v1.MasterPageBlock (*CoreServiceBinding)(nil), // 14: abi.v1.CoreServiceBinding
(*CoreServiceBinding)(nil), // 15: abi.v1.CoreServiceBinding nil, // 15: abi.v1.PluginManifest.RbacMethodRolesEntry
nil, // 16: abi.v1.PluginManifest.RbacMethodRolesEntry nil, // 16: abi.v1.CssManifest.NpmPackagesEntry
nil, // 17: abi.v1.CssManifest.NpmPackagesEntry nil, // 17: abi.v1.DirectoryExtensions.BadgeLabelsEntry
nil, // 18: abi.v1.DirectoryExtensions.BadgeLabelsEntry nil, // 18: abi.v1.CoreServiceBinding.MethodRolesEntry
nil, // 19: abi.v1.CoreServiceBinding.MethodRolesEntry
} }
var file_v1_manifest_proto_depIdxs = []int32{ var file_v1_manifest_proto_depIdxs = []int32{
3, // 0: abi.v1.PluginManifest.dependencies:type_name -> abi.v1.Dependency 2, // 0: abi.v1.PluginManifest.dependencies:type_name -> abi.v1.Dependency
4, // 1: abi.v1.PluginManifest.blocks:type_name -> abi.v1.BlockMeta 3, // 1: abi.v1.PluginManifest.blocks:type_name -> abi.v1.BlockMeta
5, // 2: abi.v1.PluginManifest.block_template_overrides:type_name -> abi.v1.BlockTemplateOverride 4, // 2: abi.v1.PluginManifest.block_template_overrides:type_name -> abi.v1.BlockTemplateOverride
6, // 3: abi.v1.PluginManifest.system_templates:type_name -> abi.v1.SystemTemplateMeta 5, // 3: abi.v1.PluginManifest.system_templates:type_name -> abi.v1.SystemTemplateMeta
7, // 4: abi.v1.PluginManifest.page_templates:type_name -> abi.v1.PageTemplateMeta 6, // 4: abi.v1.PluginManifest.page_templates:type_name -> abi.v1.PageTemplateMeta
8, // 5: abi.v1.PluginManifest.admin_pages:type_name -> abi.v1.AdminPage 7, // 5: abi.v1.PluginManifest.admin_pages:type_name -> abi.v1.AdminPage
13, // 6: abi.v1.PluginManifest.master_pages:type_name -> abi.v1.MasterPageDefinition 12, // 6: abi.v1.PluginManifest.master_pages:type_name -> abi.v1.MasterPageDefinition
9, // 7: abi.v1.PluginManifest.ai_actions:type_name -> abi.v1.AiAction 8, // 7: abi.v1.PluginManifest.ai_actions:type_name -> abi.v1.AiAction
16, // 8: abi.v1.PluginManifest.rbac_method_roles:type_name -> abi.v1.PluginManifest.RbacMethodRolesEntry 15, // 8: abi.v1.PluginManifest.rbac_method_roles:type_name -> abi.v1.PluginManifest.RbacMethodRolesEntry
10, // 9: abi.v1.PluginManifest.css_manifest:type_name -> abi.v1.CssManifest 9, // 9: abi.v1.PluginManifest.css_manifest:type_name -> abi.v1.CssManifest
11, // 10: abi.v1.PluginManifest.directory_extensions:type_name -> abi.v1.DirectoryExtensions 10, // 10: abi.v1.PluginManifest.directory_extensions:type_name -> abi.v1.DirectoryExtensions
15, // 11: abi.v1.PluginManifest.core_service_bindings:type_name -> abi.v1.CoreServiceBinding 14, // 11: abi.v1.PluginManifest.core_service_bindings:type_name -> abi.v1.CoreServiceBinding
2, // 12: abi.v1.PluginManifest.public_routes:type_name -> abi.v1.PublicRoute 1, // 12: abi.v1.PluginManifest.public_routes:type_name -> abi.v1.PublicRoute
1, // 13: abi.v1.PluginManifest.mcp_tools:type_name -> abi.v1.MCPToolDescriptor 16, // 13: abi.v1.CssManifest.npm_packages:type_name -> abi.v1.CssManifest.NpmPackagesEntry
17, // 14: abi.v1.CssManifest.npm_packages:type_name -> abi.v1.CssManifest.NpmPackagesEntry 17, // 14: abi.v1.DirectoryExtensions.badge_labels:type_name -> abi.v1.DirectoryExtensions.BadgeLabelsEntry
18, // 15: abi.v1.DirectoryExtensions.badge_labels:type_name -> abi.v1.DirectoryExtensions.BadgeLabelsEntry 13, // 15: abi.v1.MasterPageDefinition.blocks:type_name -> abi.v1.MasterPageBlock
14, // 16: abi.v1.MasterPageDefinition.blocks:type_name -> abi.v1.MasterPageBlock 18, // 16: abi.v1.CoreServiceBinding.method_roles:type_name -> abi.v1.CoreServiceBinding.MethodRolesEntry
19, // 17: abi.v1.CoreServiceBinding.method_roles:type_name -> abi.v1.CoreServiceBinding.MethodRolesEntry 11, // 17: abi.v1.DirectoryExtensions.BadgeLabelsEntry.value:type_name -> abi.v1.BadgeLabel
12, // 18: abi.v1.DirectoryExtensions.BadgeLabelsEntry.value:type_name -> abi.v1.BadgeLabel 18, // [18:18] is the sub-list for method output_type
19, // [19:19] is the sub-list for method output_type 18, // [18:18] is the sub-list for method input_type
19, // [19:19] is the sub-list for method input_type 18, // [18:18] is the sub-list for extension type_name
19, // [19:19] is the sub-list for extension type_name 18, // [18:18] is the sub-list for extension extendee
19, // [19:19] is the sub-list for extension extendee 0, // [0:18] is the sub-list for field type_name
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() }
@ -1731,14 +1619,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[14].OneofWrappers = []any{} file_v1_manifest_proto_msgTypes[13].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: 20, NumMessages: 19,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@ -1,31 +0,0 @@
# Plugin manifests publish MCP descriptors
CMS plugins expose ConnectRPC services with method-role mappings. The host can
turn those unary methods into MCP tools, but a wasm plugin previously carried
only its RBAC map; the host had no protobuf method descriptions or input
schemas from which to build a safe MCP catalogue.
Decision: `PluginManifest` carries repeated `MCPToolDescriptor` values at field
38. The Go guest binding derives descriptors from linked protobuf reflection,
includes only mapped unary methods, emits JSON object input schemas, and omits
methods it cannot describe. The host remains responsible for validating every
descriptor against the manifest RBAC map before registration.
Generating descriptors from handwritten plugin metadata was rejected because
it would duplicate protobuf definitions and drift from the actual service.
Publishing every reflected method was rejected because tools without explicit
RBAC mappings would widen the plugin's callable surface.
Consequences:
- Wasm plugin manifests can advertise their own ConnectRPC methods as MCP tools.
- Descriptor capture fails closed for missing services, streaming methods, and
malformed or unsupported protobuf inputs.
- Consumers must use pluginsdk v0.3.2 or newer before calling
`PluginManifest.GetMcpTools`.
- `abi/proto/v1/manifest.proto` remains the language-neutral source of truth;
`abi/v1/manifest.pb.go` is generated output.
Keywords: PluginManifest, MCPToolDescriptor, mcp_tools, GetMcpTools,
captureMCPTools, plugin/wasmguest/mcp.go, abi/proto/v1/manifest.proto,
abi/v1/manifest.pb.go, ConnectRPC, MCP, RBAC, pluginsdk v0.3.2

View File

@ -247,14 +247,12 @@ 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
} }
methodRoles := svc.MethodRoles() for method, role := range 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)...)
} }
} }

View File

@ -1,206 +0,0 @@
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

@ -1,100 +0,0 @@
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}, Span: []int32{1, 0, 1, 1}, 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)
}
}