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:
@@ -55,6 +55,20 @@ func ParseMarkdown(markdown string) (Node, error) {
|
||||
}, 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 {
|
||||
source []byte
|
||||
marks []Mark
|
||||
@@ -238,7 +252,7 @@ func (c *converter) convertFencedCodeBlock(n *ast.FencedCodeBlock) ([]Node, erro
|
||||
buf.Write(line.Value(c.source))
|
||||
}
|
||||
|
||||
content := buf.String()
|
||||
content := normalizeCodeBlockContent(buf.String())
|
||||
|
||||
var lang *string
|
||||
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))
|
||||
}
|
||||
|
||||
content := buf.String()
|
||||
content := normalizeCodeBlockContent(buf.String())
|
||||
|
||||
attrs, err := json.Marshal(CodeBlockAttrs{Language: nil})
|
||||
if err != nil {
|
||||
|
||||
@@ -167,7 +167,7 @@ func TestParseMarkdown_CodeBlock(t *testing.T) {
|
||||
assert.Equal(t, "go", *attrs.Language)
|
||||
|
||||
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) {
|
||||
@@ -183,6 +183,21 @@ func TestParseMarkdown_CodeBlock(t *testing.T) {
|
||||
attrs, err := cb.CodeBlockAttrs()
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2067,12 +2067,17 @@ func (r *Resolver) AddDocumentTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
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(
|
||||
ctx,
|
||||
probo.CreateDocumentRequest{
|
||||
OrganizationID: input.OrganizationID,
|
||||
Title: input.Title,
|
||||
Content: input.Content,
|
||||
Content: contentJSON,
|
||||
Classification: input.Classification,
|
||||
DocumentType: input.DocumentType,
|
||||
TrustCenterVisibility: trustCenterVisibility,
|
||||
@@ -2082,7 +2087,12 @@ func (r *Resolver) AddDocumentTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
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) {
|
||||
@@ -2129,7 +2139,12 @@ func (r *Resolver) ListDocumentVersionsTool(ctx context.Context, req *mcp.CallTo
|
||||
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) {
|
||||
@@ -2142,8 +2157,13 @@ func (r *Resolver) GetDocumentVersionTool(ctx context.Context, req *mcp.CallTool
|
||||
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{
|
||||
DocumentVersion: types.NewDocumentVersion(version),
|
||||
DocumentVersion: dv,
|
||||
}, 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{
|
||||
DocumentVersion: types.NewDocumentVersion(draftVersion),
|
||||
DocumentVersion: dv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2207,8 +2232,13 @@ func (r *Resolver) UpdateDocumentVersionTool(ctx context.Context, req *mcp.CallT
|
||||
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{
|
||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||
DocumentVersion: dv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -3835,8 +3865,13 @@ func (r *Resolver) RequestDocumentVersionApprovalTool(ctx context.Context, req *
|
||||
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{
|
||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||
DocumentVersion: dv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -3856,9 +3891,14 @@ func (r *Resolver) PublishMajorDocumentVersionTool(ctx context.Context, req *mcp
|
||||
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{
|
||||
Document: types.NewDocument(document),
|
||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||
DocumentVersion: dv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -3878,9 +3918,14 @@ func (r *Resolver) PublishMinorDocumentVersionTool(ctx context.Context, req *mcp
|
||||
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{
|
||||
Document: types.NewDocument(document),
|
||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||
DocumentVersion: dv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5327,7 +5327,7 @@ components:
|
||||
description: Document type
|
||||
content:
|
||||
type: string
|
||||
description: Document content
|
||||
description: Document body in markdown (converted from stored ProseMirror JSON)
|
||||
changelog:
|
||||
type: string
|
||||
description: Changelog
|
||||
@@ -5481,7 +5481,7 @@ components:
|
||||
description: Document title
|
||||
content:
|
||||
type: string
|
||||
description: Document content
|
||||
description: Document content in markdown format
|
||||
classification:
|
||||
$ref: "#/components/schemas/DocumentClassification"
|
||||
description: Document classification
|
||||
@@ -5494,6 +5494,7 @@ components:
|
||||
|
||||
AddDocumentOutput:
|
||||
type: object
|
||||
description: Created document and version; document_version.content is markdown
|
||||
required:
|
||||
- document
|
||||
- document_version
|
||||
@@ -5580,6 +5581,7 @@ components:
|
||||
|
||||
ListDocumentVersionsOutput:
|
||||
type: object
|
||||
description: Each document_versions[].content is markdown
|
||||
required:
|
||||
- document_versions
|
||||
properties:
|
||||
@@ -5602,6 +5604,7 @@ components:
|
||||
|
||||
GetDocumentVersionOutput:
|
||||
type: object
|
||||
description: document_version.content is markdown
|
||||
required:
|
||||
- document_version
|
||||
properties:
|
||||
@@ -5622,6 +5625,7 @@ components:
|
||||
|
||||
CreateDraftDocumentVersionOutput:
|
||||
type: object
|
||||
description: Created draft; document_version.content is markdown
|
||||
required:
|
||||
- document_version
|
||||
properties:
|
||||
@@ -5648,6 +5652,7 @@ components:
|
||||
|
||||
UpdateDocumentVersionOutput:
|
||||
type: object
|
||||
description: Updated draft; document_version.content is markdown
|
||||
required:
|
||||
- document_version
|
||||
properties:
|
||||
@@ -5698,6 +5703,7 @@ components:
|
||||
|
||||
PublishDocumentVersionOutput:
|
||||
type: object
|
||||
description: document_version.content is markdown
|
||||
required:
|
||||
- document
|
||||
- document_version
|
||||
@@ -5727,6 +5733,7 @@ components:
|
||||
|
||||
RequestDocumentVersionApprovalOutput:
|
||||
type: object
|
||||
description: document_version.content is markdown
|
||||
required:
|
||||
- document_version
|
||||
properties:
|
||||
|
||||
@@ -15,10 +15,32 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"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 {
|
||||
return &Document{
|
||||
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{
|
||||
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{
|
||||
ID: dv.ID,
|
||||
OrganizationID: dv.OrganizationID,
|
||||
@@ -105,19 +137,23 @@ func NewDocumentVersion(dv *coredata.DocumentVersion) *DocumentVersion {
|
||||
Minor: dv.Minor,
|
||||
Classification: dv.Classification,
|
||||
DocumentType: dv.DocumentType,
|
||||
Content: dv.Content,
|
||||
Content: contentMD,
|
||||
Changelog: dv.Changelog,
|
||||
Status: dv.Status,
|
||||
PublishedAt: dv.PublishedAt,
|
||||
CreatedAt: dv.CreatedAt,
|
||||
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))
|
||||
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
|
||||
@@ -129,7 +165,7 @@ func NewListDocumentVersionsOutput(versionPage *page.Page[*coredata.DocumentVers
|
||||
return ListDocumentVersionsOutput{
|
||||
NextCursor: nextCursor,
|
||||
DocumentVersions: versions,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewDocumentVersionSignature(dvs *coredata.DocumentVersionSignature) *DocumentVersionSignature {
|
||||
|
||||
Reference in New Issue
Block a user