Update MCP to handle markdown for all document content tools + fix converter edge case for code fence closing

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-06 15:04:55 +04:00
parent 24a9459fb9
commit 6d2e9dbe19
5 changed files with 140 additions and 23 deletions

View File

@@ -55,6 +55,20 @@ func ParseMarkdown(markdown string) (Node, error) {
}, nil }, nil
} }
// normalizeCodeBlockContent strips one trailing newline when it is only the
// line terminator goldmark attaches to the last content line. That avoids an
// extra visible blank line in editors (e.g. TipTap) while preserving a
// trailing blank line in the source, which ends with two newlines.
func normalizeCodeBlockContent(content string) string {
if content == "" {
return content
}
if strings.HasSuffix(content, "\n") && !strings.HasSuffix(content, "\n\n") {
return strings.TrimSuffix(content, "\n")
}
return content
}
type converter struct { type converter struct {
source []byte source []byte
marks []Mark marks []Mark
@@ -238,7 +252,7 @@ func (c *converter) convertFencedCodeBlock(n *ast.FencedCodeBlock) ([]Node, erro
buf.Write(line.Value(c.source)) buf.Write(line.Value(c.source))
} }
content := buf.String() content := normalizeCodeBlockContent(buf.String())
var lang *string var lang *string
if n.Language(c.source) != nil { if n.Language(c.source) != nil {
@@ -274,7 +288,7 @@ func (c *converter) convertCodeBlock(n *ast.CodeBlock) ([]Node, error) {
buf.Write(line.Value(c.source)) buf.Write(line.Value(c.source))
} }
content := buf.String() content := normalizeCodeBlockContent(buf.String())
attrs, err := json.Marshal(CodeBlockAttrs{Language: nil}) attrs, err := json.Marshal(CodeBlockAttrs{Language: nil})
if err != nil { if err != nil {

View File

@@ -167,7 +167,7 @@ func TestParseMarkdown_CodeBlock(t *testing.T) {
assert.Equal(t, "go", *attrs.Language) assert.Equal(t, "go", *attrs.Language)
require.Len(t, cb.Content, 1) require.Len(t, cb.Content, 1)
assert.Equal(t, "fmt.Println(\"hello\")\n", *cb.Content[0].Text) assert.Equal(t, "fmt.Println(\"hello\")", *cb.Content[0].Text)
}) })
t.Run("without language", func(t *testing.T) { t.Run("without language", func(t *testing.T) {
@@ -183,6 +183,21 @@ func TestParseMarkdown_CodeBlock(t *testing.T) {
attrs, err := cb.CodeBlockAttrs() attrs, err := cb.CodeBlockAttrs()
require.NoError(t, err) require.NoError(t, err)
assert.Nil(t, attrs.Language) assert.Nil(t, attrs.Language)
require.Len(t, cb.Content, 1)
assert.Equal(t, "some code", *cb.Content[0].Text)
})
t.Run("trailing blank line preserved", func(t *testing.T) {
t.Parallel()
doc, err := ParseMarkdown("```\nline\n\n```")
require.NoError(t, err)
require.Len(t, doc.Content, 1)
cb := doc.Content[0]
require.Len(t, cb.Content, 1)
assert.Equal(t, "line\n\n", *cb.Content[0].Text)
}) })
} }

View File

@@ -2067,12 +2067,17 @@ func (r *Resolver) AddDocumentTool(ctx context.Context, req *mcp.CallToolRequest
trustCenterVisibility = input.TrustCenterVisibility trustCenterVisibility = input.TrustCenterVisibility
} }
contentJSON, err := markdownToProseMirrorJSON(input.Content)
if err != nil {
panic(fmt.Errorf("cannot convert markdown to prosemirror: %w", err))
}
document, documentVersion, err := svc.Documents.Create( document, documentVersion, err := svc.Documents.Create(
ctx, ctx,
probo.CreateDocumentRequest{ probo.CreateDocumentRequest{
OrganizationID: input.OrganizationID, OrganizationID: input.OrganizationID,
Title: input.Title, Title: input.Title,
Content: input.Content, Content: contentJSON,
Classification: input.Classification, Classification: input.Classification,
DocumentType: input.DocumentType, DocumentType: input.DocumentType,
TrustCenterVisibility: trustCenterVisibility, TrustCenterVisibility: trustCenterVisibility,
@@ -2082,7 +2087,12 @@ func (r *Resolver) AddDocumentTool(ctx context.Context, req *mcp.CallToolRequest
panic(fmt.Errorf("cannot create document: %w", err)) panic(fmt.Errorf("cannot create document: %w", err))
} }
return nil, types.NewAddDocumentOutput(document, documentVersion), nil out, err := types.NewAddDocumentOutput(document, documentVersion)
if err != nil {
panic(fmt.Errorf("cannot build add document output: %w", err))
}
return nil, out, nil
} }
func (r *Resolver) UpdateDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateDocumentInput) (*mcp.CallToolResult, types.UpdateDocumentOutput, error) { func (r *Resolver) UpdateDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateDocumentInput) (*mcp.CallToolResult, types.UpdateDocumentOutput, error) {
@@ -2129,7 +2139,12 @@ func (r *Resolver) ListDocumentVersionsTool(ctx context.Context, req *mcp.CallTo
panic(fmt.Errorf("cannot list document versions: %w", err)) panic(fmt.Errorf("cannot list document versions: %w", err))
} }
return nil, types.NewListDocumentVersionsOutput(versionPage), nil out, err := types.NewListDocumentVersionsOutput(versionPage)
if err != nil {
panic(fmt.Errorf("cannot build list document versions output: %w", err))
}
return nil, out, nil
} }
func (r *Resolver) GetDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetDocumentVersionInput) (*mcp.CallToolResult, types.GetDocumentVersionOutput, error) { func (r *Resolver) GetDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetDocumentVersionInput) (*mcp.CallToolResult, types.GetDocumentVersionOutput, error) {
@@ -2142,8 +2157,13 @@ func (r *Resolver) GetDocumentVersionTool(ctx context.Context, req *mcp.CallTool
panic(fmt.Errorf("cannot get document version: %w", err)) panic(fmt.Errorf("cannot get document version: %w", err))
} }
dv, err := types.NewDocumentVersion(version)
if err != nil {
panic(fmt.Errorf("cannot build document version: %w", err))
}
return nil, types.GetDocumentVersionOutput{ return nil, types.GetDocumentVersionOutput{
DocumentVersion: types.NewDocumentVersion(version), DocumentVersion: dv,
}, nil }, nil
} }
@@ -2175,8 +2195,13 @@ func (r *Resolver) CreateDraftDocumentVersionTool(ctx context.Context, req *mcp.
} }
} }
dv, err := types.NewDocumentVersion(draftVersion)
if err != nil {
panic(fmt.Errorf("cannot build document version: %w", err))
}
return nil, types.CreateDraftDocumentVersionOutput{ return nil, types.CreateDraftDocumentVersionOutput{
DocumentVersion: types.NewDocumentVersion(draftVersion), DocumentVersion: dv,
}, nil }, nil
} }
@@ -2207,8 +2232,13 @@ func (r *Resolver) UpdateDocumentVersionTool(ctx context.Context, req *mcp.CallT
panic(fmt.Errorf("cannot update document version: %w", err)) panic(fmt.Errorf("cannot update document version: %w", err))
} }
dv, err := types.NewDocumentVersion(documentVersion)
if err != nil {
panic(fmt.Errorf("cannot build document version: %w", err))
}
return nil, types.UpdateDocumentVersionOutput{ return nil, types.UpdateDocumentVersionOutput{
DocumentVersion: types.NewDocumentVersion(documentVersion), DocumentVersion: dv,
}, nil }, nil
} }
@@ -3835,8 +3865,13 @@ func (r *Resolver) RequestDocumentVersionApprovalTool(ctx context.Context, req *
panic(fmt.Errorf("cannot get document version: %w", err)) panic(fmt.Errorf("cannot get document version: %w", err))
} }
dv, err := types.NewDocumentVersion(documentVersion)
if err != nil {
panic(fmt.Errorf("cannot build document version: %w", err))
}
return nil, types.RequestDocumentVersionApprovalOutput{ return nil, types.RequestDocumentVersionApprovalOutput{
DocumentVersion: types.NewDocumentVersion(documentVersion), DocumentVersion: dv,
}, nil }, nil
} }
@@ -3856,9 +3891,14 @@ func (r *Resolver) PublishMajorDocumentVersionTool(ctx context.Context, req *mcp
panic(fmt.Errorf("cannot publish major document version: %w", err)) panic(fmt.Errorf("cannot publish major document version: %w", err))
} }
dv, err := types.NewDocumentVersion(documentVersion)
if err != nil {
panic(fmt.Errorf("cannot build document version: %w", err))
}
return nil, types.PublishMajorDocumentVersionOutput{ return nil, types.PublishMajorDocumentVersionOutput{
Document: types.NewDocument(document), Document: types.NewDocument(document),
DocumentVersion: types.NewDocumentVersion(documentVersion), DocumentVersion: dv,
}, nil }, nil
} }
@@ -3878,9 +3918,14 @@ func (r *Resolver) PublishMinorDocumentVersionTool(ctx context.Context, req *mcp
panic(fmt.Errorf("cannot publish minor document version: %w", err)) panic(fmt.Errorf("cannot publish minor document version: %w", err))
} }
dv, err := types.NewDocumentVersion(documentVersion)
if err != nil {
panic(fmt.Errorf("cannot build document version: %w", err))
}
return nil, types.PublishMinorDocumentVersionOutput{ return nil, types.PublishMinorDocumentVersionOutput{
Document: types.NewDocument(document), Document: types.NewDocument(document),
DocumentVersion: types.NewDocumentVersion(documentVersion), DocumentVersion: dv,
}, nil }, nil
} }

View File

@@ -5327,7 +5327,7 @@ components:
description: Document type description: Document type
content: content:
type: string type: string
description: Document content description: Document body in markdown (converted from stored ProseMirror JSON)
changelog: changelog:
type: string type: string
description: Changelog description: Changelog
@@ -5481,7 +5481,7 @@ components:
description: Document title description: Document title
content: content:
type: string type: string
description: Document content description: Document content in markdown format
classification: classification:
$ref: "#/components/schemas/DocumentClassification" $ref: "#/components/schemas/DocumentClassification"
description: Document classification description: Document classification
@@ -5494,6 +5494,7 @@ components:
AddDocumentOutput: AddDocumentOutput:
type: object type: object
description: Created document and version; document_version.content is markdown
required: required:
- document - document
- document_version - document_version
@@ -5580,6 +5581,7 @@ components:
ListDocumentVersionsOutput: ListDocumentVersionsOutput:
type: object type: object
description: Each document_versions[].content is markdown
required: required:
- document_versions - document_versions
properties: properties:
@@ -5602,6 +5604,7 @@ components:
GetDocumentVersionOutput: GetDocumentVersionOutput:
type: object type: object
description: document_version.content is markdown
required: required:
- document_version - document_version
properties: properties:
@@ -5622,6 +5625,7 @@ components:
CreateDraftDocumentVersionOutput: CreateDraftDocumentVersionOutput:
type: object type: object
description: Created draft; document_version.content is markdown
required: required:
- document_version - document_version
properties: properties:
@@ -5648,6 +5652,7 @@ components:
UpdateDocumentVersionOutput: UpdateDocumentVersionOutput:
type: object type: object
description: Updated draft; document_version.content is markdown
required: required:
- document_version - document_version
properties: properties:
@@ -5698,6 +5703,7 @@ components:
PublishDocumentVersionOutput: PublishDocumentVersionOutput:
type: object type: object
description: document_version.content is markdown
required: required:
- document - document
- document_version - document_version
@@ -5727,6 +5733,7 @@ components:
RequestDocumentVersionApprovalOutput: RequestDocumentVersionApprovalOutput:
type: object type: object
description: document_version.content is markdown
required: required:
- document_version - document_version
properties: properties:

View File

@@ -15,10 +15,32 @@
package types package types
import ( import (
"fmt"
"strings"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/prosemirror"
) )
func proseMirrorJSONToMarkdown(pmJSON string) (string, error) {
if strings.TrimSpace(pmJSON) == "" {
return "", nil
}
node, err := prosemirror.Parse(pmJSON)
if err != nil {
return "", fmt.Errorf("cannot parse prosemirror json: %w", err)
}
md, err := prosemirror.RenderMarkdown(node)
if err != nil {
return "", fmt.Errorf("cannot render markdown: %w", err)
}
return md, nil
}
func NewDocument(d *coredata.Document) *Document { func NewDocument(d *coredata.Document) *Document {
return &Document{ return &Document{
ID: d.ID, ID: d.ID,
@@ -88,14 +110,24 @@ func NewListDocumentsOutput(documentPage *page.Page[*coredata.Document, coredata
} }
} }
func NewAddDocumentOutput(doc *coredata.Document, docVersion *coredata.DocumentVersion) AddDocumentOutput { func NewAddDocumentOutput(doc *coredata.Document, docVersion *coredata.DocumentVersion) (AddDocumentOutput, error) {
dv, err := NewDocumentVersion(docVersion)
if err != nil {
return AddDocumentOutput{}, err
}
return AddDocumentOutput{ return AddDocumentOutput{
Document: NewDocument(doc), Document: NewDocument(doc),
DocumentVersion: NewDocumentVersion(docVersion), DocumentVersion: dv,
} }, nil
} }
func NewDocumentVersion(dv *coredata.DocumentVersion) *DocumentVersion { func NewDocumentVersion(dv *coredata.DocumentVersion) (*DocumentVersion, error) {
contentMD, err := proseMirrorJSONToMarkdown(dv.Content)
if err != nil {
return nil, fmt.Errorf("cannot convert document version content to markdown: %w", err)
}
return &DocumentVersion{ return &DocumentVersion{
ID: dv.ID, ID: dv.ID,
OrganizationID: dv.OrganizationID, OrganizationID: dv.OrganizationID,
@@ -105,19 +137,23 @@ func NewDocumentVersion(dv *coredata.DocumentVersion) *DocumentVersion {
Minor: dv.Minor, Minor: dv.Minor,
Classification: dv.Classification, Classification: dv.Classification,
DocumentType: dv.DocumentType, DocumentType: dv.DocumentType,
Content: dv.Content, Content: contentMD,
Changelog: dv.Changelog, Changelog: dv.Changelog,
Status: dv.Status, Status: dv.Status,
PublishedAt: dv.PublishedAt, PublishedAt: dv.PublishedAt,
CreatedAt: dv.CreatedAt, CreatedAt: dv.CreatedAt,
UpdatedAt: dv.UpdatedAt, UpdatedAt: dv.UpdatedAt,
} }, nil
} }
func NewListDocumentVersionsOutput(versionPage *page.Page[*coredata.DocumentVersion, coredata.DocumentVersionOrderField]) ListDocumentVersionsOutput { func NewListDocumentVersionsOutput(versionPage *page.Page[*coredata.DocumentVersion, coredata.DocumentVersionOrderField]) (ListDocumentVersionsOutput, error) {
versions := make([]*DocumentVersion, 0, len(versionPage.Data)) versions := make([]*DocumentVersion, 0, len(versionPage.Data))
for _, v := range versionPage.Data { for _, v := range versionPage.Data {
versions = append(versions, NewDocumentVersion(v)) dv, err := NewDocumentVersion(v)
if err != nil {
return ListDocumentVersionsOutput{}, err
}
versions = append(versions, dv)
} }
var nextCursor *page.CursorKey var nextCursor *page.CursorKey
@@ -129,7 +165,7 @@ func NewListDocumentVersionsOutput(versionPage *page.Page[*coredata.DocumentVers
return ListDocumentVersionsOutput{ return ListDocumentVersionsOutput{
NextCursor: nextCursor, NextCursor: nextCursor,
DocumentVersions: versions, DocumentVersions: versions,
} }, nil
} }
func NewDocumentVersionSignature(dvs *coredata.DocumentVersionSignature) *DocumentVersionSignature { func NewDocumentVersionSignature(dvs *coredata.DocumentVersionSignature) *DocumentVersionSignature {