Compare commits

..

2 Commits

Author SHA1 Message Date
Alex Dunmow
1ee4db6f0d fix(test): validate MCP descriptor source locations 2026-08-10 21:10:54 +08:00
Alex Dunmow
911f3f70e0 feat: publish plugin MCP descriptors 2026-08-09 13:00:33 +08:00
6 changed files with 562 additions and 91 deletions

View File

@ -156,6 +156,26 @@ message PluginManifest {
// HTTP handler) and merges them into the site sitemap. Requires
// has_http_handler.
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).

View File

@ -149,7 +149,14 @@ type PluginManifest struct {
// entries from the plugin's well-known sitemap endpoint (served by its
// HTTP handler) and merges them into the site sitemap. Requires
// 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
sizeCache protoimpl.SizeCache
}
@ -443,6 +450,100 @@ func (x *PluginManifest) GetSitemap() bool {
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).
type PublicRoute struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -459,7 +560,7 @@ type PublicRoute struct {
func (x *PublicRoute) Reset() {
*x = PublicRoute{}
mi := &file_v1_manifest_proto_msgTypes[1]
mi := &file_v1_manifest_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -471,7 +572,7 @@ func (x *PublicRoute) String() string {
func (*PublicRoute) ProtoMessage() {}
func (x *PublicRoute) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[1]
mi := &file_v1_manifest_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -484,7 +585,7 @@ func (x *PublicRoute) ProtoReflect() protoreflect.Message {
// Deprecated: Use PublicRoute.ProtoReflect.Descriptor instead.
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 {
@ -513,7 +614,7 @@ type Dependency struct {
func (x *Dependency) Reset() {
*x = Dependency{}
mi := &file_v1_manifest_proto_msgTypes[2]
mi := &file_v1_manifest_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -525,7 +626,7 @@ func (x *Dependency) String() string {
func (*Dependency) ProtoMessage() {}
func (x *Dependency) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[2]
mi := &file_v1_manifest_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -538,7 +639,7 @@ func (x *Dependency) ProtoReflect() protoreflect.Message {
// Deprecated: Use Dependency.ProtoReflect.Descriptor instead.
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 {
@ -581,7 +682,7 @@ type BlockMeta struct {
func (x *BlockMeta) Reset() {
*x = BlockMeta{}
mi := &file_v1_manifest_proto_msgTypes[3]
mi := &file_v1_manifest_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -593,7 +694,7 @@ func (x *BlockMeta) String() string {
func (*BlockMeta) ProtoMessage() {}
func (x *BlockMeta) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[3]
mi := &file_v1_manifest_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -606,7 +707,7 @@ func (x *BlockMeta) ProtoReflect() protoreflect.Message {
// Deprecated: Use BlockMeta.ProtoReflect.Descriptor instead.
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 {
@ -672,7 +773,7 @@ type BlockTemplateOverride struct {
func (x *BlockTemplateOverride) Reset() {
*x = BlockTemplateOverride{}
mi := &file_v1_manifest_proto_msgTypes[4]
mi := &file_v1_manifest_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -684,7 +785,7 @@ func (x *BlockTemplateOverride) String() string {
func (*BlockTemplateOverride) ProtoMessage() {}
func (x *BlockTemplateOverride) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[4]
mi := &file_v1_manifest_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -697,7 +798,7 @@ func (x *BlockTemplateOverride) ProtoReflect() protoreflect.Message {
// Deprecated: Use BlockTemplateOverride.ProtoReflect.Descriptor instead.
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 {
@ -733,7 +834,7 @@ type SystemTemplateMeta struct {
func (x *SystemTemplateMeta) Reset() {
*x = SystemTemplateMeta{}
mi := &file_v1_manifest_proto_msgTypes[5]
mi := &file_v1_manifest_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -745,7 +846,7 @@ func (x *SystemTemplateMeta) String() string {
func (*SystemTemplateMeta) ProtoMessage() {}
func (x *SystemTemplateMeta) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[5]
mi := &file_v1_manifest_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -758,7 +859,7 @@ func (x *SystemTemplateMeta) ProtoReflect() protoreflect.Message {
// Deprecated: Use SystemTemplateMeta.ProtoReflect.Descriptor instead.
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 {
@ -797,7 +898,7 @@ type PageTemplateMeta struct {
func (x *PageTemplateMeta) Reset() {
*x = PageTemplateMeta{}
mi := &file_v1_manifest_proto_msgTypes[6]
mi := &file_v1_manifest_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -809,7 +910,7 @@ func (x *PageTemplateMeta) String() string {
func (*PageTemplateMeta) ProtoMessage() {}
func (x *PageTemplateMeta) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[6]
mi := &file_v1_manifest_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -822,7 +923,7 @@ func (x *PageTemplateMeta) ProtoReflect() protoreflect.Message {
// Deprecated: Use PageTemplateMeta.ProtoReflect.Descriptor instead.
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 {
@ -876,7 +977,7 @@ type AdminPage struct {
func (x *AdminPage) Reset() {
*x = AdminPage{}
mi := &file_v1_manifest_proto_msgTypes[7]
mi := &file_v1_manifest_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -888,7 +989,7 @@ func (x *AdminPage) String() string {
func (*AdminPage) ProtoMessage() {}
func (x *AdminPage) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[7]
mi := &file_v1_manifest_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -901,7 +1002,7 @@ func (x *AdminPage) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdminPage.ProtoReflect.Descriptor instead.
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 {
@ -953,7 +1054,7 @@ type AiAction struct {
func (x *AiAction) Reset() {
*x = AiAction{}
mi := &file_v1_manifest_proto_msgTypes[8]
mi := &file_v1_manifest_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -965,7 +1066,7 @@ func (x *AiAction) String() string {
func (*AiAction) ProtoMessage() {}
func (x *AiAction) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[8]
mi := &file_v1_manifest_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -978,7 +1079,7 @@ func (x *AiAction) ProtoReflect() protoreflect.Message {
// Deprecated: Use AiAction.ProtoReflect.Descriptor instead.
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 {
@ -1028,7 +1129,7 @@ type CssManifest struct {
func (x *CssManifest) Reset() {
*x = CssManifest{}
mi := &file_v1_manifest_proto_msgTypes[9]
mi := &file_v1_manifest_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1040,7 +1141,7 @@ func (x *CssManifest) String() string {
func (*CssManifest) ProtoMessage() {}
func (x *CssManifest) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[9]
mi := &file_v1_manifest_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1053,7 +1154,7 @@ func (x *CssManifest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CssManifest.ProtoReflect.Descriptor instead.
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 {
@ -1094,7 +1195,7 @@ type DirectoryExtensions struct {
func (x *DirectoryExtensions) Reset() {
*x = DirectoryExtensions{}
mi := &file_v1_manifest_proto_msgTypes[10]
mi := &file_v1_manifest_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1106,7 +1207,7 @@ func (x *DirectoryExtensions) String() string {
func (*DirectoryExtensions) ProtoMessage() {}
func (x *DirectoryExtensions) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[10]
mi := &file_v1_manifest_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1119,7 +1220,7 @@ func (x *DirectoryExtensions) ProtoReflect() protoreflect.Message {
// Deprecated: Use DirectoryExtensions.ProtoReflect.Descriptor instead.
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 {
@ -1168,7 +1269,7 @@ type BadgeLabel struct {
func (x *BadgeLabel) Reset() {
*x = BadgeLabel{}
mi := &file_v1_manifest_proto_msgTypes[11]
mi := &file_v1_manifest_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1180,7 +1281,7 @@ func (x *BadgeLabel) String() string {
func (*BadgeLabel) ProtoMessage() {}
func (x *BadgeLabel) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[11]
mi := &file_v1_manifest_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1193,7 +1294,7 @@ func (x *BadgeLabel) ProtoReflect() protoreflect.Message {
// Deprecated: Use BadgeLabel.ProtoReflect.Descriptor instead.
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 {
@ -1223,7 +1324,7 @@ type MasterPageDefinition struct {
func (x *MasterPageDefinition) Reset() {
*x = MasterPageDefinition{}
mi := &file_v1_manifest_proto_msgTypes[12]
mi := &file_v1_manifest_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1235,7 +1336,7 @@ func (x *MasterPageDefinition) String() string {
func (*MasterPageDefinition) ProtoMessage() {}
func (x *MasterPageDefinition) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[12]
mi := &file_v1_manifest_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1248,7 +1349,7 @@ func (x *MasterPageDefinition) ProtoReflect() protoreflect.Message {
// Deprecated: Use MasterPageDefinition.ProtoReflect.Descriptor instead.
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 {
@ -1295,7 +1396,7 @@ type MasterPageBlock struct {
func (x *MasterPageBlock) Reset() {
*x = MasterPageBlock{}
mi := &file_v1_manifest_proto_msgTypes[13]
mi := &file_v1_manifest_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1307,7 +1408,7 @@ func (x *MasterPageBlock) String() string {
func (*MasterPageBlock) ProtoMessage() {}
func (x *MasterPageBlock) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[13]
mi := &file_v1_manifest_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1320,7 +1421,7 @@ func (x *MasterPageBlock) ProtoReflect() protoreflect.Message {
// Deprecated: Use MasterPageBlock.ProtoReflect.Descriptor instead.
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 {
@ -1377,7 +1478,7 @@ type CoreServiceBinding struct {
func (x *CoreServiceBinding) Reset() {
*x = CoreServiceBinding{}
mi := &file_v1_manifest_proto_msgTypes[14]
mi := &file_v1_manifest_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1389,7 +1490,7 @@ func (x *CoreServiceBinding) String() string {
func (*CoreServiceBinding) ProtoMessage() {}
func (x *CoreServiceBinding) ProtoReflect() protoreflect.Message {
mi := &file_v1_manifest_proto_msgTypes[14]
mi := &file_v1_manifest_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1402,7 +1503,7 @@ func (x *CoreServiceBinding) ProtoReflect() protoreflect.Message {
// Deprecated: Use CoreServiceBinding.ProtoReflect.Descriptor instead.
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 {
@ -1423,7 +1524,7 @@ var File_v1_manifest_proto protoreflect.FileDescriptor
const file_v1_manifest_proto_rawDesc = "" +
"\n" +
"\x11v1/manifest.proto\x12\x06abi.v1\"\xaf\x0e\n" +
"\x11v1/manifest.proto\x12\x06abi.v1\"\xe7\x0e\n" +
"\x0ePluginManifest\x12\x1f\n" +
"\vabi_version\x18\x01 \x01(\rR\n" +
"abiVersion\x12\x12\n" +
@ -1465,10 +1566,19 @@ const file_v1_manifest_proto_rawDesc = "" +
"\rallowed_hosts\x18\" \x03(\tR\fallowedHosts\x12&\n" +
"\x0fmax_response_mb\x18# \x01(\rR\rmaxResponseMb\x128\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" +
"\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" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x16\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
}
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{
(*PluginManifest)(nil), // 0: abi.v1.PluginManifest
(*PublicRoute)(nil), // 1: abi.v1.PublicRoute
(*Dependency)(nil), // 2: abi.v1.Dependency
(*BlockMeta)(nil), // 3: abi.v1.BlockMeta
(*BlockTemplateOverride)(nil), // 4: abi.v1.BlockTemplateOverride
(*SystemTemplateMeta)(nil), // 5: abi.v1.SystemTemplateMeta
(*PageTemplateMeta)(nil), // 6: abi.v1.PageTemplateMeta
(*AdminPage)(nil), // 7: abi.v1.AdminPage
(*AiAction)(nil), // 8: abi.v1.AiAction
(*CssManifest)(nil), // 9: abi.v1.CssManifest
(*DirectoryExtensions)(nil), // 10: abi.v1.DirectoryExtensions
(*BadgeLabel)(nil), // 11: abi.v1.BadgeLabel
(*MasterPageDefinition)(nil), // 12: abi.v1.MasterPageDefinition
(*MasterPageBlock)(nil), // 13: abi.v1.MasterPageBlock
(*CoreServiceBinding)(nil), // 14: abi.v1.CoreServiceBinding
nil, // 15: abi.v1.PluginManifest.RbacMethodRolesEntry
nil, // 16: abi.v1.CssManifest.NpmPackagesEntry
nil, // 17: abi.v1.DirectoryExtensions.BadgeLabelsEntry
nil, // 18: abi.v1.CoreServiceBinding.MethodRolesEntry
(*MCPToolDescriptor)(nil), // 1: abi.v1.MCPToolDescriptor
(*PublicRoute)(nil), // 2: abi.v1.PublicRoute
(*Dependency)(nil), // 3: abi.v1.Dependency
(*BlockMeta)(nil), // 4: abi.v1.BlockMeta
(*BlockTemplateOverride)(nil), // 5: abi.v1.BlockTemplateOverride
(*SystemTemplateMeta)(nil), // 6: abi.v1.SystemTemplateMeta
(*PageTemplateMeta)(nil), // 7: abi.v1.PageTemplateMeta
(*AdminPage)(nil), // 8: abi.v1.AdminPage
(*AiAction)(nil), // 9: abi.v1.AiAction
(*CssManifest)(nil), // 10: abi.v1.CssManifest
(*DirectoryExtensions)(nil), // 11: abi.v1.DirectoryExtensions
(*BadgeLabel)(nil), // 12: abi.v1.BadgeLabel
(*MasterPageDefinition)(nil), // 13: abi.v1.MasterPageDefinition
(*MasterPageBlock)(nil), // 14: abi.v1.MasterPageBlock
(*CoreServiceBinding)(nil), // 15: abi.v1.CoreServiceBinding
nil, // 16: abi.v1.PluginManifest.RbacMethodRolesEntry
nil, // 17: abi.v1.CssManifest.NpmPackagesEntry
nil, // 18: abi.v1.DirectoryExtensions.BadgeLabelsEntry
nil, // 19: abi.v1.CoreServiceBinding.MethodRolesEntry
}
var file_v1_manifest_proto_depIdxs = []int32{
2, // 0: abi.v1.PluginManifest.dependencies:type_name -> abi.v1.Dependency
3, // 1: abi.v1.PluginManifest.blocks:type_name -> abi.v1.BlockMeta
4, // 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, // 4: abi.v1.PluginManifest.page_templates:type_name -> abi.v1.PageTemplateMeta
7, // 5: abi.v1.PluginManifest.admin_pages:type_name -> abi.v1.AdminPage
12, // 6: abi.v1.PluginManifest.master_pages:type_name -> abi.v1.MasterPageDefinition
8, // 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
9, // 9: abi.v1.PluginManifest.css_manifest:type_name -> abi.v1.CssManifest
10, // 10: abi.v1.PluginManifest.directory_extensions:type_name -> abi.v1.DirectoryExtensions
14, // 11: abi.v1.PluginManifest.core_service_bindings:type_name -> abi.v1.CoreServiceBinding
1, // 12: abi.v1.PluginManifest.public_routes:type_name -> abi.v1.PublicRoute
16, // 13: abi.v1.CssManifest.npm_packages:type_name -> abi.v1.CssManifest.NpmPackagesEntry
17, // 14: abi.v1.DirectoryExtensions.badge_labels:type_name -> abi.v1.DirectoryExtensions.BadgeLabelsEntry
13, // 15: abi.v1.MasterPageDefinition.blocks:type_name -> abi.v1.MasterPageBlock
18, // 16: abi.v1.CoreServiceBinding.method_roles:type_name -> abi.v1.CoreServiceBinding.MethodRolesEntry
11, // 17: abi.v1.DirectoryExtensions.BadgeLabelsEntry.value:type_name -> abi.v1.BadgeLabel
18, // [18:18] is the sub-list for method output_type
18, // [18:18] is the sub-list for method input_type
18, // [18:18] is the sub-list for extension type_name
18, // [18:18] is the sub-list for extension extendee
0, // [0:18] is the sub-list for field type_name
3, // 0: abi.v1.PluginManifest.dependencies:type_name -> abi.v1.Dependency
4, // 1: abi.v1.PluginManifest.blocks:type_name -> abi.v1.BlockMeta
5, // 2: abi.v1.PluginManifest.block_template_overrides:type_name -> abi.v1.BlockTemplateOverride
6, // 3: abi.v1.PluginManifest.system_templates:type_name -> abi.v1.SystemTemplateMeta
7, // 4: abi.v1.PluginManifest.page_templates:type_name -> abi.v1.PageTemplateMeta
8, // 5: abi.v1.PluginManifest.admin_pages:type_name -> abi.v1.AdminPage
13, // 6: abi.v1.PluginManifest.master_pages:type_name -> abi.v1.MasterPageDefinition
9, // 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
10, // 9: abi.v1.PluginManifest.css_manifest:type_name -> abi.v1.CssManifest
11, // 10: abi.v1.PluginManifest.directory_extensions:type_name -> abi.v1.DirectoryExtensions
15, // 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, // 13: abi.v1.PluginManifest.mcp_tools:type_name -> abi.v1.MCPToolDescriptor
17, // 14: abi.v1.CssManifest.npm_packages:type_name -> abi.v1.CssManifest.NpmPackagesEntry
18, // 15: abi.v1.DirectoryExtensions.badge_labels:type_name -> abi.v1.DirectoryExtensions.BadgeLabelsEntry
14, // 16: abi.v1.MasterPageDefinition.blocks:type_name -> abi.v1.MasterPageBlock
19, // 17: abi.v1.CoreServiceBinding.method_roles:type_name -> abi.v1.CoreServiceBinding.MethodRolesEntry
12, // 18: abi.v1.DirectoryExtensions.BadgeLabelsEntry.value:type_name -> abi.v1.BadgeLabel
19, // [19:19] is the sub-list for method output_type
19, // [19:19] is the sub-list for method input_type
19, // [19:19] is the sub-list for extension 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() }
@ -1619,14 +1731,14 @@ func file_v1_manifest_proto_init() {
if File_v1_manifest_proto != nil {
return
}
file_v1_manifest_proto_msgTypes[13].OneofWrappers = []any{}
file_v1_manifest_proto_msgTypes[14].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_manifest_proto_rawDesc), len(file_v1_manifest_proto_rawDesc)),
NumEnums: 0,
NumMessages: 19,
NumMessages: 20,
NumExtensions: 0,
NumServices: 0,
},

View File

@ -0,0 +1,31 @@
# 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,12 +247,14 @@ func (g *guest) captureServiceRegistration(m *abiv1.PluginManifest) {
if coreBound[svc.Name()] {
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 {
m.RbacMethodRoles = make(map[string]string)
}
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}, 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)
}
}