mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-20 19:36:08 +00:00
feat(mcp): persist and display marketplace README
Capture the README markdown from LangBot Space when installing an MCP server and store it on the mcp_servers record (new readme column + alembic migration 0004). The detail page can then render docs offline, independent of the server's runtime/connection state.
This commit is contained in:
@@ -11,6 +11,10 @@ class MCPServer(Base):
|
|||||||
enable = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False)
|
enable = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False)
|
||||||
mode = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) # stdio, sse, http
|
mode = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) # stdio, sse, http
|
||||||
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
||||||
|
# Markdown documentation captured from LangBot Space at install time so the
|
||||||
|
# detail page can show docs even when the server is offline / has no tools.
|
||||||
|
# Empty string for manually-created servers that have no marketplace README.
|
||||||
|
readme = sqlalchemy.Column(sqlalchemy.Text, nullable=False, server_default='', default='')
|
||||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
||||||
updated_at = sqlalchemy.Column(
|
updated_at = sqlalchemy.Column(
|
||||||
sqlalchemy.DateTime,
|
sqlalchemy.DateTime,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""add readme column to mcp_servers
|
||||||
|
|
||||||
|
Revision ID: 0004_add_mcp_readme
|
||||||
|
Revises: 0003_add_rerank_models
|
||||||
|
Create Date: 2026-06-06
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = '0004_add_mcp_readme'
|
||||||
|
down_revision = '0003_add_rerank_models'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add ``readme`` to mcp_servers if the table exists and the column is missing
|
||||||
|
# (the table may have been created by create_all() with the column already
|
||||||
|
# present on fresh installs, so guard against duplicate-add).
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = sa.inspect(conn)
|
||||||
|
if 'mcp_servers' not in inspector.get_table_names():
|
||||||
|
return
|
||||||
|
columns = {col['name'] for col in inspector.get_columns('mcp_servers')}
|
||||||
|
if 'readme' not in columns:
|
||||||
|
op.add_column(
|
||||||
|
'mcp_servers',
|
||||||
|
sa.Column('readme', sa.Text(), nullable=False, server_default=''),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('mcp_servers', 'readme')
|
||||||
@@ -248,6 +248,9 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
|||||||
|
|
||||||
mode = mcp_data.get('mode') or 'stdio'
|
mode = mcp_data.get('mode') or 'stdio'
|
||||||
extra_args = mcp_data.get('extra_args') or {}
|
extra_args = mcp_data.get('extra_args') or {}
|
||||||
|
# Marketplace records carry the rendered README markdown; persist it so
|
||||||
|
# the detail page Docs tab works offline and without a marketplace round-trip.
|
||||||
|
readme = mcp_data.get('readme') or ''
|
||||||
# Use __ instead of / to avoid URL routing issues with slashes
|
# Use __ instead of / to avoid URL routing issues with slashes
|
||||||
name = f'{mcp_data.get("author", "")}__{mcp_data.get("name", "")}'
|
name = f'{mcp_data.get("author", "")}__{mcp_data.get("name", "")}'
|
||||||
|
|
||||||
@@ -267,6 +270,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
|||||||
'enable': True,
|
'enable': True,
|
||||||
'mode': mode,
|
'mode': mode,
|
||||||
'extra_args': extra_args,
|
'extra_args': extra_args,
|
||||||
|
'readme': readme,
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))
|
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
|
import rehypeSlug from 'rehype-slug';
|
||||||
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import '@/styles/github-markdown.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the README markdown captured from LangBot Space at install time.
|
||||||
|
* The README is stored on the MCP server record (``server.readme``) so this
|
||||||
|
* works offline and regardless of the server's runtime/connection state.
|
||||||
|
*
|
||||||
|
* MCP marketplace READMEs reference images by absolute URL (the upstream repo),
|
||||||
|
* so — unlike plugin READMEs — no asset-path rewriting is needed here.
|
||||||
|
*/
|
||||||
|
export default function MCPReadme({ readme }: { readme?: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!readme || !readme.trim()) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[220px] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
|
||||||
|
{t('mcp.noReadme')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full overflow-auto">
|
||||||
|
<div className="markdown-body max-w-none p-1 pt-0">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[
|
||||||
|
rehypeRaw,
|
||||||
|
rehypeSanitize,
|
||||||
|
rehypeHighlight,
|
||||||
|
rehypeSlug,
|
||||||
|
[
|
||||||
|
rehypeAutolinkHeadings,
|
||||||
|
{
|
||||||
|
behavior: 'wrap',
|
||||||
|
properties: {
|
||||||
|
className: ['anchor'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]}
|
||||||
|
components={{
|
||||||
|
ul: ({ children }) => <ul className="list-disc">{children}</ul>,
|
||||||
|
ol: ({ children }) => <ol className="list-decimal">{children}</ol>,
|
||||||
|
li: ({ children }) => <li className="ml-4">{children}</li>,
|
||||||
|
a: ({ children, href, ...props }) => (
|
||||||
|
<a href={href} target="_blank" rel="noopener noreferrer" {...props}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
img: ({ src, alt, ...props }) => (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt || ''}
|
||||||
|
className="my-4 h-auto max-w-full rounded-lg"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{readme}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -551,6 +551,7 @@ export type MCPServer =
|
|||||||
enable: boolean;
|
enable: boolean;
|
||||||
extra_args: MCPServerExtraArgsSSE;
|
extra_args: MCPServerExtraArgsSSE;
|
||||||
runtime_info?: MCPServerRuntimeInfo;
|
runtime_info?: MCPServerRuntimeInfo;
|
||||||
|
readme?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
@@ -561,6 +562,7 @@ export type MCPServer =
|
|||||||
enable: boolean;
|
enable: boolean;
|
||||||
extra_args: MCPServerExtraArgsHttp;
|
extra_args: MCPServerExtraArgsHttp;
|
||||||
runtime_info?: MCPServerRuntimeInfo;
|
runtime_info?: MCPServerRuntimeInfo;
|
||||||
|
readme?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
@@ -571,6 +573,7 @@ export type MCPServer =
|
|||||||
enable: boolean;
|
enable: boolean;
|
||||||
extra_args: MCPServerExtraArgsStdio;
|
extra_args: MCPServerExtraArgsStdio;
|
||||||
runtime_info?: MCPServerRuntimeInfo;
|
runtime_info?: MCPServerRuntimeInfo;
|
||||||
|
readme?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user