Skip to content

Commit e488f80

Browse files
author
Owl Builder
committed
Allow MCP dictionary searches to return Markdown
The search_dictionary MCP tool now accepts an optional format parameter. Omitting it preserves the existing JSON text output, while format=markdown converts each HTML definition through html-to-markdown and returns Markdown text content for clients that prefer readable dictionary output. Constraint: Frontend copy behavior is intentionally unchanged for this iteration Constraint: format must remain optional so existing MCP clients continue working Rejected: Changing the default MCP output to Markdown | would break clients expecting the current JSON text content Confidence: high Scope-risk: moderate Directive: Keep format handling backward-compatible; add new formats explicitly instead of changing the default Tested: cd backend && go test ./... Tested: cd backend && go vet ./... Tested: git diff --check
1 parent 38d6acf commit e488f80

6 files changed

Lines changed: 156 additions & 20 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ The initial SSE connection must include the token. After the connection is estab
116116
- Searches dictionaries available to the token owner.
117117
- Accepts `query`.
118118
- Optional: `dictionary_id` or `dictionary_name`.
119+
- Optional: `format=markdown` returns Markdown text content for the MCP response; omit `format` to keep the default JSON text output.
119120
- If no dictionary is specified, Owl searches all dictionaries available to the token user, matching the web search scope.
120121

121122
### Token management

README_ZH.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ Authorization: Bearer <MCP_TOKEN>
117117
- 查询当前 token 用户可访问的词典。
118118
- 必填:`query`
119119
- 可选:`dictionary_id``dictionary_name`
120+
- 可选:`format=markdown` 会让 MCP 响应的文本内容以 Markdown 输出;不传 `format` 时保持默认 JSON 文本输出。
120121
- 如果不指定词典,则按 Web 查询相同范围搜索全部可访问词典。
121122

122123
### Token 管理

backend/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.25.0
44

55
require (
66
entgo.io/ent v0.14.6
7+
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0
78
github.com/go-sql-driver/mysql v1.9.3
89
github.com/golang-jwt/jwt/v5 v5.3.1
910
github.com/labstack/echo/v5 v5.1.0
@@ -18,6 +19,7 @@ require (
1819
require (
1920
ariga.io/atlas v0.36.2-0.20250730182955-2c6300d0a3e1 // indirect
2021
filippo.io/edwards25519 v1.1.0 // indirect
22+
github.com/JohannesKaufmann/dom v0.2.0 // indirect
2123
github.com/agext/levenshtein v1.2.3 // indirect
2224
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
2325
github.com/bmatcuk/doublestar v1.3.4 // indirect
@@ -47,6 +49,7 @@ require (
4749
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
4850
go.uber.org/atomic v1.11.0 // indirect
4951
golang.org/x/mod v0.34.0 // indirect
52+
golang.org/x/net v0.52.0 // indirect
5053
golang.org/x/oauth2 v0.35.0 // indirect
5154
golang.org/x/sys v0.43.0 // indirect
5255
golang.org/x/text v0.36.0 // indirect

backend/go.sum

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
66
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
77
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
88
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
9+
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
10+
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
11+
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0 h1:mklaPbT4f/EiDr1Q+zPrEt9lgKAkVrIBtWf33d9GpVA=
12+
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0/go.mod h1:D56Cl9r8M5i3UwAchE+LlLc5hPN3kJtdZNVJn06lSHU=
913
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
1014
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
1115
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
@@ -20,8 +24,6 @@ github.com/c0mm4nd/go-ripemd v0.0.0-20200326052756-bd1759ad7d10 h1:wJ2csnFApV9G1
2024
github.com/c0mm4nd/go-ripemd v0.0.0-20200326052756-bd1759ad7d10/go.mod h1:mYPR+a1fzjnHY3VFH5KL3PkEjMlVfGXP7c8rbWlkLJg=
2125
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
2226
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
23-
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
24-
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
2527
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
2628
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
2729
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
@@ -89,14 +91,6 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
8991
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
9092
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
9193
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
92-
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
93-
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
94-
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
95-
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
96-
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
97-
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
98-
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
99-
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
10094
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
10195
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
10296
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -112,16 +106,14 @@ github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE=
112106
github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU=
113107
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
114108
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
109+
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
110+
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
115111
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
116112
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
117113
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
118114
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
119-
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
120-
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
121-
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
122-
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
123-
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
124-
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
115+
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
116+
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
125117
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
126118
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
127119
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -134,6 +126,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
134126
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
135127
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
136128
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
129+
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
130+
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
137131
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
138132
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
139133
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=

backend/internal/api/mcp.go

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"owl/backend/internal/dictionary"
1111
"owl/backend/pkg/version"
1212

13+
htmltomarkdown "github.com/JohannesKaufmann/html-to-markdown/v2"
1314
echo "github.com/labstack/echo/v5"
1415
"github.com/modelcontextprotocol/go-sdk/mcp"
1516
)
@@ -35,6 +36,7 @@ type listDictionariesOutput struct {
3536
type searchDictionaryInput struct {
3637
DictionaryID *int `json:"dictionary_id,omitempty" jsonschema:"Optional dictionary id returned by list_dictionaries. If omitted, all accessible dictionaries are searched."`
3738
DictionaryName *string `json:"dictionary_name,omitempty" jsonschema:"Optional dictionary name or title returned by list_dictionaries. Used when dictionary_id is omitted."`
39+
Format *string `json:"format,omitempty" jsonschema:"Optional text output format. Use markdown to return Markdown text content; omit to keep the default JSON text output."`
3840
Query string `json:"query" jsonschema:"Word, phrase, or headword to look up."`
3941
}
4042

@@ -44,6 +46,7 @@ type searchResultInfo struct {
4446
Visibility string `json:"visibility"`
4547
Word string `json:"word"`
4648
HTML string `json:"html"`
49+
Markdown string `json:"markdown,omitempty"`
4750
Score float64 `json:"score"`
4851
Source string `json:"source"`
4952
}
@@ -146,6 +149,10 @@ func (s *Server) buildMCPServer(userID int) *mcp.Server {
146149
if query == "" {
147150
return nil, searchDictionaryOutput{}, fmt.Errorf("query is required")
148151
}
152+
format, err := normalizeMCPSearchFormat(input.Format)
153+
if err != nil {
154+
return nil, searchDictionaryOutput{}, err
155+
}
149156
dictionaryID := 0
150157
if input.DictionaryID != nil {
151158
dictionaryID = *input.DictionaryID
@@ -154,7 +161,7 @@ func (s *Server) buildMCPServer(userID int) *mcp.Server {
154161
if input.DictionaryName != nil {
155162
dictionaryName = *input.DictionaryName
156163
}
157-
dictionaryID, err := s.resolveMCPDictionaryID(ctx, userID, dictionaryID, dictionaryName)
164+
dictionaryID, err = s.resolveMCPDictionaryID(ctx, userID, dictionaryID, dictionaryName)
158165
if err != nil {
159166
return nil, searchDictionaryOutput{}, err
160167
}
@@ -163,16 +170,28 @@ func (s *Server) buildMCPServer(userID int) *mcp.Server {
163170
return nil, searchDictionaryOutput{}, err
164171
}
165172
out := searchDictionaryOutput{Results: make([]searchResultInfo, 0, len(results))}
173+
markdownRequested := format == "markdown"
166174
for _, result := range results {
167-
out.Results = append(out.Results, searchResultInfo{
175+
item := searchResultInfo{
168176
DictionaryID: result.DictionaryID,
169177
DictionaryName: result.DictionaryName,
170178
Visibility: result.Visibility,
171179
Word: result.Word,
172180
HTML: result.HTML,
173181
Score: result.Score,
174182
Source: result.Source,
175-
})
183+
}
184+
if markdownRequested {
185+
markdown, err := htmlToMarkdown(result.HTML)
186+
if err != nil {
187+
return nil, searchDictionaryOutput{}, fmt.Errorf("convert %q to markdown: %w", result.Word, err)
188+
}
189+
item.Markdown = markdown
190+
}
191+
out.Results = append(out.Results, item)
192+
}
193+
if markdownRequested {
194+
return mcpMarkdownSearchResult(query, out), out, nil
176195
}
177196
return mcpTextResult(out), out, nil
178197
})
@@ -220,6 +239,54 @@ func (s *Server) resolveMCPDictionaryID(ctx context.Context, userID int, diction
220239
return 0, fmt.Errorf("dictionary %q is not available to this token", name)
221240
}
222241

242+
func normalizeMCPSearchFormat(value *string) (string, error) {
243+
if value == nil {
244+
return "", nil
245+
}
246+
format := strings.ToLower(strings.TrimSpace(*value))
247+
switch format {
248+
case "", "json":
249+
return "", nil
250+
case "markdown":
251+
return "markdown", nil
252+
default:
253+
return "", fmt.Errorf("unsupported format %q; omit format or use markdown", *value)
254+
}
255+
}
256+
257+
func htmlToMarkdown(html string) (string, error) {
258+
markdown, err := htmltomarkdown.ConvertString(html)
259+
if err != nil {
260+
return "", err
261+
}
262+
return strings.TrimSpace(markdown), nil
263+
}
264+
265+
func mcpMarkdownSearchResult(query string, out searchDictionaryOutput) *mcp.CallToolResult {
266+
var builder strings.Builder
267+
if strings.TrimSpace(query) != "" {
268+
fmt.Fprintf(&builder, "# Search results for %s\n\n", query)
269+
} else {
270+
builder.WriteString("# Search results\n\n")
271+
}
272+
if len(out.Results) == 0 {
273+
builder.WriteString("No matching entries found.")
274+
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: builder.String()}}}
275+
}
276+
for index, result := range out.Results {
277+
if index > 0 {
278+
builder.WriteString("\n\n---\n\n")
279+
}
280+
fmt.Fprintf(&builder, "## %s\n\n", result.Word)
281+
fmt.Fprintf(&builder, "> Dictionary: %s \n", result.DictionaryName)
282+
fmt.Fprintf(&builder, "> Visibility: %s \n", result.Visibility)
283+
fmt.Fprintf(&builder, "> Source: %s \n", result.Source)
284+
fmt.Fprintf(&builder, "> Score: %.4g\n\n", result.Score)
285+
builder.WriteString(strings.TrimSpace(result.Markdown))
286+
}
287+
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: strings.TrimSpace(builder.String())}}}
288+
}
289+
223290
func mcpTextResult(value any) *mcp.CallToolResult {
224291
payload, err := json.MarshalIndent(value, "", " ")
225292
if err != nil {

backend/internal/api/mcp_schema_test.go

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"context"
55
"encoding/json"
6+
"strings"
67
"testing"
78

89
"github.com/modelcontextprotocol/go-sdk/mcp"
@@ -49,9 +50,78 @@ func TestMCPSearchDictionaryOptionalDictionaryInput(t *testing.T) {
4950
t.Fatal(err)
5051
}
5152
for _, field := range schema.Required {
52-
if field == "dictionary_id" || field == "dictionary_name" {
53+
if field == "dictionary_id" || field == "dictionary_name" || field == "format" {
5354
t.Fatalf("%s should be optional in schema: %s", field, string(encoded))
5455
}
5556
}
5657

5758
}
59+
60+
func TestNormalizeMCPSearchFormatKeepsFormatOptional(t *testing.T) {
61+
format, err := normalizeMCPSearchFormat(nil)
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
if format != "" {
66+
t.Fatalf("expected omitted format to keep default output, got %q", format)
67+
}
68+
69+
markdown := " markdown "
70+
format, err = normalizeMCPSearchFormat(&markdown)
71+
if err != nil {
72+
t.Fatal(err)
73+
}
74+
if format != "markdown" {
75+
t.Fatalf("expected markdown format, got %q", format)
76+
}
77+
78+
jsonFormat := "json"
79+
format, err = normalizeMCPSearchFormat(&jsonFormat)
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
if format != "" {
84+
t.Fatalf("expected json format to keep default output, got %q", format)
85+
}
86+
87+
invalid := "html"
88+
if _, err := normalizeMCPSearchFormat(&invalid); err == nil {
89+
t.Fatal("expected unsupported format to fail")
90+
}
91+
}
92+
93+
func TestHTMLToMarkdownConvertsDictionaryHTML(t *testing.T) {
94+
markdown, err := htmlToMarkdown(`<p><strong>Bold</strong> text</p><ul><li>one</li></ul><a href="/search?q=owl">owl</a>`)
95+
if err != nil {
96+
t.Fatal(err)
97+
}
98+
for _, expected := range []string{"**Bold** text", "- one", "[owl](/search?q=owl)"} {
99+
if !strings.Contains(markdown, expected) {
100+
t.Fatalf("expected markdown to contain %q, got:\n%s", expected, markdown)
101+
}
102+
}
103+
}
104+
105+
func TestMCPMarkdownSearchResultUsesMarkdownText(t *testing.T) {
106+
result := mcpMarkdownSearchResult("owl", searchDictionaryOutput{Results: []searchResultInfo{{
107+
DictionaryID: 1,
108+
DictionaryName: "Test Dictionary",
109+
Visibility: "public",
110+
Word: "owl",
111+
Markdown: "A **bird**.",
112+
Score: 1,
113+
Source: "exact",
114+
}}})
115+
if len(result.Content) != 1 {
116+
t.Fatalf("expected one text content item, got %d", len(result.Content))
117+
}
118+
textContent, ok := result.Content[0].(*mcp.TextContent)
119+
if !ok {
120+
t.Fatalf("expected text content, got %T", result.Content[0])
121+
}
122+
for _, expected := range []string{"# Search results for owl", "## owl", "> Dictionary: Test Dictionary", "A **bird**."} {
123+
if !strings.Contains(textContent.Text, expected) {
124+
t.Fatalf("expected MCP markdown text to contain %q, got:\n%s", expected, textContent.Text)
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)