feat: publish plugin MCP descriptors
This commit is contained in:
parent
bf72cd160a
commit
911f3f70e0
@ -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).
|
||||
|
||||
@ -150,6 +150,13 @@ type PluginManifest struct {
|
||||
// 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"`
|
||||
// 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,
|
||||
},
|
||||
|
||||
@ -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
206
plugin/wasmguest/mcp.go
Normal 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)
|
||||
}
|
||||
}
|
||||
100
plugin/wasmguest/mcp_test.go
Normal file
100
plugin/wasmguest/mcp_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user