Skip to content

Commit 3eb330c

Browse files
committed
feat: add support for text-only content in MCP tools
Cursor removed support for MCP Resources in version 0.46.9+, breaking compatibility with Firebolt’s MCP server which relied heavily on resource embedding. To restore functionality, introduced the `--disable-resources` CLI flag that allows `Docs` and `Connect` tools to return plain text responses instead of embedded resources.
1 parent 175ed4b commit 3eb330c

File tree

6 files changed

+178
-22
lines changed

6 files changed

+178
-22
lines changed

cmd/firebolt-mcp-server/main.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ func main() {
6363
Usage: "SSE transport listen address (used only if transport is set to sse)",
6464
Sources: cli.EnvVars("FIREBOLT_MCP_TRANSPORT_SSE_LISTEN_ADDRESS"),
6565
},
66+
&cli.BoolFlag{
67+
Name: "disable-resources",
68+
Category: "MCP Transport",
69+
Value: false,
70+
Usage: "Return text content instead of embedded resources (for clients that do not support resources)",
71+
Sources: cli.EnvVars("FIREBOLT_MCP_DISABLE_RESOURCES"),
72+
},
6673
&cli.StringFlag{
6774
Name: "client-id",
6875
Category: "Firebolt Authentication",
@@ -123,6 +130,7 @@ func run(ctx context.Context, cmd *cli.Command) error {
123130

124131
// Initialize MCP server
125132
docsProof := generateRandomSecret()
133+
disableResources := cmd.Bool("disable-resources")
126134
resourceDocs := resources.NewDocs(fireboltdocs.FS, docsProof)
127135
resourceAccounts := resources.NewAccounts(discoveryClient)
128136
resourceDatabases := resources.NewDatabases(dbPool)
@@ -133,8 +141,8 @@ func run(ctx context.Context, cmd *cli.Command) error {
133141
cmd.String("transport"),
134142
cmd.String("transport-sse-listen-address"),
135143
[]server.Tool{
136-
tools.NewConnect(resourceAccounts, resourceDatabases, resourceEngines, docsProof),
137-
tools.NewDocs(resourceDocs),
144+
tools.NewConnect(resourceAccounts, resourceDatabases, resourceEngines, docsProof, disableResources),
145+
tools.NewDocs(resourceDocs, disableResources),
138146
tools.NewQuery(dbPool),
139147
},
140148
[]server.Prompt{

pkg/tools/connect.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type Connect struct {
4444
databasesFetcher DatabaseResourcesFetcher // Fetches database resources
4545
enginesFetcher EngineResourcesFetcher // Fetches engine resources
4646
docsProof string // Shared with the docs resources
47+
disableResources bool // Return text content instead of embedded resources
4748
}
4849

4950
// NewConnect creates a new instance of the Connect tool with the provided resource fetchers.
@@ -53,12 +54,14 @@ func NewConnect(
5354
databasesFetcher DatabaseResourcesFetcher,
5455
enginesFetcher EngineResourcesFetcher,
5556
docsProof string,
57+
disableResources bool,
5658
) *Connect {
5759
return &Connect{
5860
accountsFetcher: accountsFetcher,
5961
databasesFetcher: databasesFetcher,
6062
enginesFetcher: enginesFetcher,
6163
docsProof: docsProof,
64+
disableResources: disableResources,
6265
}
6366
}
6467

@@ -178,7 +181,7 @@ func (t *Connect) Handler(ctx context.Context, request mcp.CallToolRequest) (*mc
178181
return &mcp.CallToolResult{
179182
Result: mcp.Result{},
180183
Content: itertools.Map(results, func(i mcp.ResourceContents) mcp.Content {
181-
return mcp.NewEmbeddedResource(i)
184+
return textOrResourceContent(t.disableResources, i)
182185
}),
183186
IsError: false,
184187
}, nil

pkg/tools/connect_test.go

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,13 @@ func createEngineResource(accountName, engineName string) mcp.ResourceContents {
8080

8181
func TestNewConnect(t *testing.T) {
8282
mock := &MockResourceFetcher{}
83-
connectTool := tools.NewConnect(mock, mock, mock, validProof)
83+
connectTool := tools.NewConnect(mock, mock, mock, validProof, false)
8484
assert.NotNil(t, connectTool)
8585
}
8686

8787
func TestConnect_Tool(t *testing.T) {
8888
mock := &MockResourceFetcher{}
89-
connectTool := tools.NewConnect(mock, mock, mock, validProof)
89+
connectTool := tools.NewConnect(mock, mock, mock, validProof, false)
9090

9191
tool := connectTool.Tool()
9292
assert.Equal(t, "firebolt_connect", tool.Name)
@@ -131,7 +131,7 @@ func TestConnect_Handler_Success(t *testing.T) {
131131
}
132132

133133
// Create the tool
134-
connectTool := tools.NewConnect(mock, mock, mock, validProof)
134+
connectTool := tools.NewConnect(mock, mock, mock, validProof, false)
135135

136136
// Execute the handler
137137
request := mcp.CallToolRequest{}
@@ -199,7 +199,7 @@ func TestConnect_Handler_AccountFetchFailure(t *testing.T) {
199199
},
200200
}
201201

202-
connectTool := tools.NewConnect(mock, mock, mock, validProof)
202+
connectTool := tools.NewConnect(mock, mock, mock, validProof, false)
203203
request := mcp.CallToolRequest{}
204204
request.Params.Arguments = map[string]any{
205205
"docs_proof": validProof,
@@ -225,7 +225,7 @@ func TestConnect_Handler_InvalidAccountResource(t *testing.T) {
225225
},
226226
}
227227

228-
connectTool := tools.NewConnect(mock, mock, mock, validProof)
228+
connectTool := tools.NewConnect(mock, mock, mock, validProof, false)
229229
request := mcp.CallToolRequest{}
230230
request.Params.Arguments = map[string]any{
231231
"docs_proof": validProof,
@@ -251,7 +251,7 @@ func TestConnect_Handler_InvalidAccountJSON(t *testing.T) {
251251
},
252252
}
253253

254-
connectTool := tools.NewConnect(mock, mock, mock, validProof)
254+
connectTool := tools.NewConnect(mock, mock, mock, validProof, false)
255255
request := mcp.CallToolRequest{}
256256
request.Params.Arguments = map[string]any{
257257
"docs_proof": validProof,
@@ -273,7 +273,7 @@ func TestConnect_Handler_DatabasesFetchFailure(t *testing.T) {
273273
},
274274
}
275275

276-
connectTool := tools.NewConnect(mock, mock, mock, validProof)
276+
connectTool := tools.NewConnect(mock, mock, mock, validProof, false)
277277
request := mcp.CallToolRequest{}
278278
request.Params.Arguments = map[string]any{
279279
"docs_proof": validProof,
@@ -298,7 +298,7 @@ func TestConnect_Handler_EnginesFetchFailure(t *testing.T) {
298298
},
299299
}
300300

301-
connectTool := tools.NewConnect(mock, mock, mock, validProof)
301+
connectTool := tools.NewConnect(mock, mock, mock, validProof, false)
302302
request := mcp.CallToolRequest{}
303303
request.Params.Arguments = map[string]any{
304304
"docs_proof": validProof,
@@ -309,3 +309,73 @@ func TestConnect_Handler_EnginesFetchFailure(t *testing.T) {
309309
assert.Contains(t, err.Error(), "failed to discover engine resources")
310310
assert.Nil(t, result)
311311
}
312+
313+
func TestConnect_Handler_DisableResources(t *testing.T) {
314+
// Create test data
315+
accounts := []string{"account1"}
316+
databases := map[string][]string{
317+
"account1": {"db1"},
318+
}
319+
engines := map[string][]string{
320+
"account1": {"engine1"},
321+
}
322+
323+
// Create mock fetcher
324+
mock := &MockResourceFetcher{
325+
AccountsFunc: func(ctx context.Context, accountName string) ([]mcp.ResourceContents, error) {
326+
var resources []mcp.ResourceContents
327+
for _, acc := range accounts {
328+
resources = append(resources, createAccountResource(acc))
329+
}
330+
return resources, nil
331+
},
332+
DatabasesFunc: func(ctx context.Context, accountName, databaseName string) ([]mcp.ResourceContents, error) {
333+
var resources []mcp.ResourceContents
334+
for _, db := range databases[accountName] {
335+
resources = append(resources, createDatabaseResource(accountName, db))
336+
}
337+
return resources, nil
338+
},
339+
EnginesFunc: func(ctx context.Context, accountName, engineName string) ([]mcp.ResourceContents, error) {
340+
var resources []mcp.ResourceContents
341+
for _, eng := range engines[accountName] {
342+
resources = append(resources, createEngineResource(accountName, eng))
343+
}
344+
return resources, nil
345+
},
346+
}
347+
348+
// Create the tool with disableResources set to true
349+
connectTool := tools.NewConnect(mock, mock, mock, validProof, true)
350+
351+
// Execute the handler
352+
request := mcp.CallToolRequest{}
353+
request.Params.Arguments = map[string]any{
354+
"docs_proof": validProof,
355+
}
356+
result, err := connectTool.Handler(t.Context(), request)
357+
358+
// Assertions
359+
require.NoError(t, err)
360+
require.NotNil(t, result)
361+
assert.False(t, result.IsError)
362+
363+
// Calculate expected total resources
364+
expectedCount := len(accounts) // accounts
365+
for _, dbs := range databases {
366+
expectedCount += len(dbs) // databases
367+
}
368+
for _, engs := range engines {
369+
expectedCount += len(engs) // engines
370+
}
371+
372+
// Check if we got the expected number of resources
373+
assert.Len(t, result.Content, expectedCount)
374+
375+
// Verify the content contains text content instead of embedded resources
376+
for _, content := range result.Content {
377+
textContent, ok := content.(mcp.TextContent)
378+
require.True(t, ok, "Expected TextContent when disableResources is true")
379+
assert.NotEmpty(t, textContent.Text)
380+
}
381+
}

pkg/tools/docs.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ type DocsResourcesFetcher interface {
2121
// Docs represents a tool for fetching and returning Firebolt documentation.
2222
// It provides access to documentation articles that explain Firebolt concepts and functionality.
2323
type Docs struct {
24-
docsFetcher DocsResourcesFetcher // Fetches documentation resources
24+
docsFetcher DocsResourcesFetcher // Fetches documentation resources
25+
disableResources bool // Return text content instead of embedded resources
2526
}
2627

2728
// NewDocs creates a new instance of the Docs tool with the provided documentation fetcher.
2829
// It requires an implementation for fetching documentation articles.
29-
func NewDocs(docsFetcher DocsResourcesFetcher) *Docs {
30+
func NewDocs(docsFetcher DocsResourcesFetcher, disableResources bool) *Docs {
3031
return &Docs{
31-
docsFetcher: docsFetcher,
32+
docsFetcher: docsFetcher,
33+
disableResources: disableResources,
3234
}
3335
}
3436

@@ -103,7 +105,7 @@ func (t *Docs) Handler(ctx context.Context, request mcp.CallToolRequest) (*mcp.C
103105
return &mcp.CallToolResult{
104106
Result: mcp.Result{},
105107
Content: itertools.Map(results, func(i mcp.ResourceContents) mcp.Content {
106-
return mcp.NewEmbeddedResource(i)
108+
return textOrResourceContent(t.disableResources, i)
107109
}),
108110
IsError: false,
109111
}, nil

pkg/tools/docs_test.go

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import (
1616

1717
func TestNewDocs(t *testing.T) {
1818
mock := &MockDocsFetcher{}
19-
docsTool := tools.NewDocs(mock)
19+
docsTool := tools.NewDocs(mock, false)
2020
assert.NotNil(t, docsTool)
2121
}
2222

2323
func TestDocs_Tool(t *testing.T) {
2424
mock := &MockDocsFetcher{}
25-
docsTool := tools.NewDocs(mock)
25+
docsTool := tools.NewDocs(mock, false)
2626

2727
tool := docsTool.Tool()
2828
assert.Equal(t, "firebolt_docs", tool.Name)
@@ -49,7 +49,7 @@ func TestDocs_Handler_DefaultArticles(t *testing.T) {
4949
}
5050

5151
// Create the tool
52-
docsTool := tools.NewDocs(mock)
52+
docsTool := tools.NewDocs(mock, false)
5353

5454
// Execute the handler with empty request (should return default articles)
5555
request := mcp.CallToolRequest{}
@@ -103,7 +103,7 @@ func TestDocs_Handler_SpecificArticles(t *testing.T) {
103103
}
104104

105105
// Create the tool
106-
docsTool := tools.NewDocs(mock)
106+
docsTool := tools.NewDocs(mock, false)
107107

108108
// Execute the handler with specific articles
109109
request := mcp.CallToolRequest{}
@@ -149,7 +149,7 @@ func TestDocs_Handler_FetchError(t *testing.T) {
149149
}
150150

151151
// Create the tool
152-
docsTool := tools.NewDocs(mock)
152+
docsTool := tools.NewDocs(mock, false)
153153

154154
// Execute the handler
155155
request := mcp.CallToolRequest{}
@@ -165,7 +165,7 @@ func TestDocs_Handler_FetchError(t *testing.T) {
165165
func TestDocs_Handler_InvalidArticleID(t *testing.T) {
166166
// Create the tool with any mock
167167
mock := &MockDocsFetcher{}
168-
docsTool := tools.NewDocs(mock)
168+
docsTool := tools.NewDocs(mock, false)
169169

170170
// Execute the handler with an invalid article ID type
171171
request := mcp.CallToolRequest{}
@@ -195,7 +195,7 @@ func TestDocs_Handler_MultipleFetchedResources(t *testing.T) {
195195
}
196196

197197
// Create the tool
198-
docsTool := tools.NewDocs(mock)
198+
docsTool := tools.NewDocs(mock, false)
199199

200200
// Execute the handler
201201
request := mcp.CallToolRequest{}
@@ -228,6 +228,63 @@ func TestDocs_Handler_MultipleFetchedResources(t *testing.T) {
228228
assert.True(t, resourceMap["firebolt://docs/multi-resource-2"])
229229
}
230230

231+
func TestDocs_Handler_DisableResources(t *testing.T) {
232+
// Create test data for default articles
233+
mockArticles := map[string]string{
234+
resources.DocsArticleOverview: "# Firebolt Overview\nThis is an overview of Firebolt.",
235+
resources.DocsArticleProof: "# Proof Document\nSecret proof: proof_value_123",
236+
resources.DocsArticleReference: "# Reference\nThis is the reference documentation.",
237+
}
238+
239+
// Create mock fetcher that returns the mock articles
240+
mock := &MockDocsFetcher{
241+
FetchDocsFunc: func(ctx context.Context, article string) ([]mcp.ResourceContents, error) {
242+
content, exists := mockArticles[article]
243+
if !exists {
244+
return nil, errors.New("article not found")
245+
}
246+
return []mcp.ResourceContents{createDocResource(article, content)}, nil
247+
},
248+
}
249+
250+
// Create the tool with disableResources set to true
251+
docsTool := tools.NewDocs(mock, true)
252+
253+
// Execute the handler with empty request (should return default articles)
254+
request := mcp.CallToolRequest{}
255+
request.Params.Arguments = map[string]any{}
256+
result, err := docsTool.Handler(t.Context(), request)
257+
258+
// Assertions
259+
require.NoError(t, err)
260+
require.NotNil(t, result)
261+
assert.False(t, result.IsError)
262+
263+
// Should return 3 default articles
264+
assert.Len(t, result.Content, 3)
265+
266+
// Verify the content contains text content instead of embedded resources
267+
textContents := make(map[string]string)
268+
for _, content := range result.Content {
269+
textContent, ok := content.(mcp.TextContent)
270+
require.True(t, ok, "Expected TextContent when disableResources is true")
271+
assert.NotEmpty(t, textContent.Text)
272+
273+
// Store the text content for verification
274+
for articleID, expectedContent := range mockArticles {
275+
if textContent.Text == expectedContent {
276+
textContents[articleID] = textContent.Text
277+
}
278+
}
279+
}
280+
281+
// Check if all default articles are present
282+
for articleID, expectedContent := range mockArticles {
283+
assert.Contains(t, textContents, articleID)
284+
assert.Equal(t, expectedContent, textContents[articleID])
285+
}
286+
}
287+
231288
type MockDocsFetcher struct {
232289
FetchDocsFunc func(ctx context.Context, article string) ([]mcp.ResourceContents, error)
233290
}

pkg/tools/tools.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package tools
2+
3+
import "github.com/mark3labs/mcp-go/mcp"
4+
5+
// textOrResourceContent returns a text content if disableResources is true, otherwise returns an embedded resource.
6+
func textOrResourceContent(disableResources bool, i mcp.ResourceContents) mcp.Content {
7+
8+
if disableResources {
9+
textResource, ok := i.(mcp.TextResourceContents)
10+
if ok {
11+
return mcp.NewTextContent(textResource.Text)
12+
}
13+
}
14+
15+
return mcp.NewEmbeddedResource(i)
16+
}

0 commit comments

Comments
 (0)