Skip to content

Commit 7c99d53

Browse files
authored
feat(go): add MCP plugin support for resources (#3358)
1 parent 02ffc20 commit 7c99d53

File tree

18 files changed

+2101
-352
lines changed

18 files changed

+2101
-352
lines changed

go/ai/resource.go

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,69 @@ import (
2020
"context"
2121
"fmt"
2222
"maps"
23+
"net/url"
24+
"strings"
2325

2426
"github.com/firebase/genkit/go/core"
2527
"github.com/firebase/genkit/go/core/api"
26-
coreresource "github.com/firebase/genkit/go/core/resource"
28+
"github.com/yosida95/uritemplate/v3"
2729
)
2830

31+
// normalizeURI normalizes a URI for template matching by removing query parameters,
32+
// fragments, and trailing slashes from the path.
33+
func normalizeURI(rawURI string) string {
34+
// Parse the URI
35+
u, err := url.Parse(rawURI)
36+
if err != nil {
37+
// If parsing fails, return the original URI
38+
return rawURI
39+
}
40+
41+
// Remove query parameters and fragment
42+
u.RawQuery = ""
43+
u.Fragment = ""
44+
45+
// Remove trailing slash from path (but not from the root path)
46+
if len(u.Path) > 1 && strings.HasSuffix(u.Path, "/") {
47+
u.Path = strings.TrimSuffix(u.Path, "/")
48+
}
49+
50+
return u.String()
51+
}
52+
53+
// matches checks if a URI matches the given URI template.
54+
func matches(templateStr, uri string) (bool, error) {
55+
template, err := uritemplate.New(templateStr)
56+
if err != nil {
57+
return false, fmt.Errorf("invalid URI template %q: %w", templateStr, err)
58+
}
59+
60+
normalizedURI := normalizeURI(uri)
61+
values := template.Match(normalizedURI)
62+
return len(values) > 0, nil
63+
}
64+
65+
// extractVariables extracts variables from a URI using the given URI template.
66+
func extractVariables(templateStr, uri string) (map[string]string, error) {
67+
template, err := uritemplate.New(templateStr)
68+
if err != nil {
69+
return nil, fmt.Errorf("invalid URI template %q: %w", templateStr, err)
70+
}
71+
72+
normalizedURI := normalizeURI(uri)
73+
values := template.Match(normalizedURI)
74+
if len(values) == 0 {
75+
return nil, fmt.Errorf("URI %q does not match template", uri)
76+
}
77+
78+
// Convert uritemplate.Values to string map
79+
result := make(map[string]string)
80+
for name, value := range values {
81+
result[name] = value.String()
82+
}
83+
return result, nil
84+
}
85+
2986
// ResourceInput represents the input to a resource function.
3087
type ResourceInput struct {
3188
URI string `json:"uri"` // The resource URI
@@ -129,11 +186,11 @@ func (r *resource) Matches(uri string) bool {
129186

130187
// Check template
131188
if template, ok := resourceMeta["template"].(string); ok && template != "" {
132-
matcher, err := coreresource.NewTemplateMatcher(template)
189+
matches, err := matches(template, uri)
133190
if err != nil {
134191
return false
135192
}
136-
return matcher.Matches(uri)
193+
return matches
137194
}
138195

139196
return false
@@ -156,11 +213,7 @@ func (r *resource) ExtractVariables(uri string) (map[string]string, error) {
156213

157214
// Extract from template
158215
if template, ok := resourceMeta["template"].(string); ok && template != "" {
159-
matcher, err := coreresource.NewTemplateMatcher(template)
160-
if err != nil {
161-
return nil, fmt.Errorf("invalid template %q: %w", template, err)
162-
}
163-
return matcher.ExtractVariables(uri)
216+
return extractVariables(template, uri)
164217
}
165218

166219
return nil, fmt.Errorf("no URI or template found in resource metadata")

go/core/resource/matcher.go

Lines changed: 0 additions & 84 deletions
This file was deleted.

go/genkit/genkit.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,15 +1022,12 @@ func NewResource(name string, opts *ai.ResourceOptions, fn ai.ResourceFunc) ai.R
10221022
func ListResources(g *Genkit) []ai.Resource {
10231023
acts := g.reg.ListActions()
10241024
resources := []ai.Resource{}
1025-
for _, act := range acts {
1026-
action := act
1025+
for _, action := range acts {
10271026
actionDesc := action.Desc()
10281027
if actionDesc.Type == api.ActionTypeResource {
1029-
// Look up the resource wrapper
1030-
if resourceValue := g.reg.LookupValue(fmt.Sprintf("resource/%s", actionDesc.Name)); resourceValue != nil {
1031-
if resource, ok := resourceValue.(ai.Resource); ok {
1032-
resources = append(resources, resource)
1033-
}
1028+
resource := ai.LookupResource(g.reg, actionDesc.Name)
1029+
if resource != nil {
1030+
resources = append(resources, resource)
10341031
}
10351032
}
10361033
}

go/plugins/mcp/common.go

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package mcp
1717

1818
import (
1919
"fmt"
20-
"strings"
2120

2221
"github.com/mark3labs/mcp-go/mcp"
2322
)
@@ -32,19 +31,9 @@ func (c *GenkitMCPClient) GetToolNameWithNamespace(toolName string) string {
3231
return fmt.Sprintf("%s_%s", c.options.Name, toolName)
3332
}
3433

35-
// ContentToText extracts text content from MCP Content
36-
func ContentToText(contentList []mcp.Content) string {
37-
var textParts []string
38-
for _, contentItem := range contentList {
39-
if textContent, ok := contentItem.(mcp.TextContent); ok && textContent.Type == "text" {
40-
textParts = append(textParts, textContent.Text)
41-
} else if erContent, ok := contentItem.(mcp.EmbeddedResource); ok {
42-
if trc, ok := erContent.Resource.(mcp.TextResourceContents); ok {
43-
textParts = append(textParts, trc.Text)
44-
}
45-
}
46-
}
47-
return strings.Join(textParts, "")
34+
// GetResourceNameWithNamespace returns a resource name prefixed with the client's namespace
35+
func (c *GenkitMCPClient) GetResourceNameWithNamespace(resourceName string) string {
36+
return fmt.Sprintf("%s_%s", c.options.Name, resourceName)
4837
}
4938

5039
// ExtractTextFromContent extracts text content from MCP Content
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
20+
"github.com/firebase/genkit/go/ai"
21+
"github.com/firebase/genkit/go/genkit"
22+
"github.com/firebase/genkit/go/plugins/mcp"
23+
)
24+
25+
func main() {
26+
g := genkit.Init(context.Background())
27+
genkit.DefineResource(g, "test-docs", &ai.ResourceOptions{
28+
Template: "file://test/{filename}",
29+
}, func(ctx context.Context, input *ai.ResourceInput) (*ai.ResourceOutput, error) {
30+
return &ai.ResourceOutput{
31+
Content: []*ai.Part{ai.NewTextPart("test content")},
32+
}, nil
33+
})
34+
35+
server := mcp.NewMCPServer(g, mcp.MCPServerOptions{Name: "test"})
36+
server.ServeStdio()
37+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
"github.com/firebase/genkit/go/ai"
22+
"github.com/firebase/genkit/go/genkit"
23+
"github.com/firebase/genkit/go/plugins/mcp"
24+
)
25+
26+
func main() {
27+
g := genkit.Init(context.Background())
28+
29+
// Resource that provides different content based on filename
30+
genkit.DefineResource(g, "content-provider", &ai.ResourceOptions{
31+
Template: "file://data/{filename}",
32+
}, func(ctx context.Context, input *ai.ResourceInput) (*ai.ResourceOutput, error) {
33+
filename := input.Variables["filename"]
34+
content := fmt.Sprintf("CONTENT_FROM_SERVER: This is %s with important data.", filename)
35+
return &ai.ResourceOutput{
36+
Content: []*ai.Part{ai.NewTextPart(content)},
37+
}, nil
38+
})
39+
40+
server := mcp.NewMCPServer(g, mcp.MCPServerOptions{Name: "content-server"})
41+
server.ServeStdio()
42+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
20+
"github.com/firebase/genkit/go/ai"
21+
"github.com/firebase/genkit/go/genkit"
22+
"github.com/firebase/genkit/go/plugins/mcp"
23+
)
24+
25+
func main() {
26+
g := genkit.Init(context.Background())
27+
genkit.DefineResource(g, "company-policy", &ai.ResourceOptions{
28+
Template: "docs://policy/{section}",
29+
}, func(ctx context.Context, input *ai.ResourceInput) (*ai.ResourceOutput, error) {
30+
return &ai.ResourceOutput{
31+
Content: []*ai.Part{ai.NewTextPart("VACATION_POLICY: Employees get 20 days vacation per year.")},
32+
}, nil
33+
})
34+
server := mcp.NewMCPServer(g, mcp.MCPServerOptions{Name: "test"})
35+
server.ServeStdio()
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
20+
"github.com/firebase/genkit/go/ai"
21+
"github.com/firebase/genkit/go/genkit"
22+
"github.com/firebase/genkit/go/plugins/mcp"
23+
)
24+
25+
func main() {
26+
g := genkit.Init(context.Background())
27+
genkit.DefineResource(g, "server-a-docs", &ai.ResourceOptions{
28+
Template: "a://docs/{file}",
29+
}, func(ctx context.Context, input *ai.ResourceInput) (*ai.ResourceOutput, error) {
30+
return &ai.ResourceOutput{
31+
Content: []*ai.Part{ai.NewTextPart("Content from Server A")},
32+
}, nil
33+
})
34+
server := mcp.NewMCPServer(g, mcp.MCPServerOptions{Name: "server-a"})
35+
server.ServeStdio()
36+
}

0 commit comments

Comments
 (0)