@mdedit/mcp-server
Advanced tools
+97
-4
@@ -0,4 +1,6 @@ | ||
| import { ArticleSession, AgentCredentialProvider } from '@mdedit/agent-client'; | ||
| import { IArticle, PublishArticleRequest, PublishArticleResponse, PublishStatusResponse, UnpublishArticleResponse } from '@mdedit/sdk'; | ||
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
| import { ArticleSession } from '@mdedit/agent-client'; | ||
| import { IncomingMessage, ServerResponse } from 'node:http'; | ||
| import { OAuthCredentialStore } from '@mdedit/node-auth'; | ||
@@ -21,3 +23,35 @@ type OpenArticleSession = (workspaceId: string, articleId: string) => Promise<ArticleSession>; | ||
| declare const MDEDIT_MCP_TOOL_SCOPES: { | ||
| readonly list_articles: readonly ["articles:read"]; | ||
| readonly create_article: readonly ["articles:write"]; | ||
| readonly read_article: readonly ["articles:read"]; | ||
| readonly publish_article: readonly ["publishing:write"]; | ||
| readonly get_publish_status: readonly ["publishing:read"]; | ||
| readonly unpublish_article: readonly ["publishing:write"]; | ||
| readonly edit_article: readonly ["articles:write"]; | ||
| readonly add_comment: readonly ["articles:read", "reviews:write"]; | ||
| readonly add_suggestion: readonly ["articles:read", "reviews:write"]; | ||
| readonly reply_to_thread: readonly ["articles:read", "reviews:write"]; | ||
| readonly resolve_thread: readonly ["articles:read", "reviews:write"]; | ||
| readonly list_review_threads: readonly ["articles:read", "reviews:read"]; | ||
| readonly get_presence: readonly ["articles:read"]; | ||
| }; | ||
| interface CreateMcpArticleInput { | ||
| collaborative: boolean; | ||
| content: string; | ||
| folderId?: string; | ||
| title: string; | ||
| workspaceId: string; | ||
| } | ||
| interface CreateMcpArticleResult extends Record<string, unknown> { | ||
| articleId: string; | ||
| content: string; | ||
| contentHash: string; | ||
| contentRevision: number; | ||
| editorUrl: string; | ||
| title: string; | ||
| workspaceId: string; | ||
| } | ||
| type MdeditMcpDependencies = { | ||
| createArticle?(input: CreateMcpArticleInput): Promise<CreateMcpArticleResult>; | ||
| listArticles(workspaceId: string): Promise<IArticle[]>; | ||
@@ -35,3 +69,3 @@ readArticle(workspaceId: string, articleId: string): Promise<IArticle>; | ||
| agent_source: 'mcp'; | ||
| transport?: 'rest' | 'stdio' | 'websocket'; | ||
| transport?: 'rest' | 'stdio' | 'streamable-http' | 'websocket'; | ||
| tool_name?: string; | ||
@@ -48,3 +82,5 @@ duration_ms?: number; | ||
| type CreateMdeditMcpServerOptions = { | ||
| apiKey: string; | ||
| apiKey?: string; | ||
| availableScopes?: readonly string[]; | ||
| credentialProvider?: AgentCredentialProvider; | ||
| apiUrl?: string; | ||
@@ -55,2 +91,4 @@ agentName?: string; | ||
| telemetry?: McpTelemetry; | ||
| transport?: 'stdio' | 'streamable-http'; | ||
| signal?: AbortSignal; | ||
| }; | ||
@@ -61,4 +99,59 @@ type MdeditMcpRuntime = { | ||
| }; | ||
| interface CreateArticleSdk { | ||
| article: { | ||
| create(workspaceId: string, input: { | ||
| articleId: string; | ||
| title: string; | ||
| folderId?: string | null; | ||
| collaborative?: boolean; | ||
| }): Promise<unknown>; | ||
| delete(workspaceId: string, articleId: string): Promise<unknown>; | ||
| get(workspaceId: string, articleId: string): Promise<IArticle>; | ||
| saveContent(workspaceId: string, articleId: string, content: string, input?: { | ||
| baseContentRevision?: number; | ||
| contentHash?: string; | ||
| folderId?: string | null; | ||
| title?: string; | ||
| }): Promise<{ | ||
| contentRevision?: number; | ||
| } | void>; | ||
| }; | ||
| collaboration: { | ||
| checkpoint(workspaceId: string, articleId: string): Promise<{ | ||
| packageRevision?: number; | ||
| targetHashes: Record<string, string>; | ||
| yjsRevision?: number; | ||
| }>; | ||
| }; | ||
| } | ||
| declare function createDurableMcpArticle(input: CreateMcpArticleInput, { openCollaborativeSession, sdk, }: { | ||
| openCollaborativeSession: (workspaceId: string, articleId: string) => Promise<ArticleSession>; | ||
| sdk: CreateArticleSdk; | ||
| }): Promise<CreateMcpArticleResult>; | ||
| declare function createMdeditMcpServer(options: CreateMdeditMcpServerOptions): MdeditMcpRuntime; | ||
| type StreamableHttpRequest = IncomingMessage & { | ||
| body?: unknown; | ||
| }; | ||
| type StreamableHttpOptionsFactory = (request: StreamableHttpRequest, signal: AbortSignal) => CreateMdeditMcpServerOptions | Promise<CreateMdeditMcpServerOptions>; | ||
| declare function createStreamableHttpHandler({ optionsForRequest, }: { | ||
| optionsForRequest: StreamableHttpOptionsFactory; | ||
| }): (request: StreamableHttpRequest, response: ServerResponse) => Promise<void>; | ||
| interface SharedCliConfig { | ||
| apiUrl?: string; | ||
| oauthProfile?: string; | ||
| oauthProfiles?: string[]; | ||
| } | ||
| declare function readSharedCliOAuthConfig(environment: Record<string, string | undefined>): SharedCliConfig; | ||
| declare function createSharedOAuthCredentialProvider({ credentialStore, environment, fetch, sharedConfig, }?: { | ||
| credentialStore?: OAuthCredentialStore; | ||
| environment?: Record<string, string | undefined>; | ||
| fetch?: typeof globalThis.fetch; | ||
| sharedConfig?: SharedCliConfig; | ||
| }): Promise<{ | ||
| availableScopes: string[]; | ||
| credentialProvider: AgentCredentialProvider; | ||
| }>; | ||
| type McpEnvironment = Record<string, string | undefined>; | ||
@@ -69,2 +162,2 @@ declare function optionsFromEnvironment(environment?: McpEnvironment): CreateMdeditMcpServerOptions; | ||
| export { ArticleSessionPool, type ArticleSessionPoolOptions, type CreateMdeditMcpServerOptions, type McpEnvironment, type McpTelemetry, type McpTelemetryEvent, type MdeditMcpDependencies, type MdeditMcpRuntime, type OpenArticleSession, createMdeditMcpServer, optionsFromEnvironment, runStdioServer, runStdioServerFromEnvironment }; | ||
| export { ArticleSessionPool, type ArticleSessionPoolOptions, type CreateMcpArticleInput, type CreateMcpArticleResult, type CreateMdeditMcpServerOptions, MDEDIT_MCP_TOOL_SCOPES, type McpEnvironment, type McpTelemetry, type McpTelemetryEvent, type MdeditMcpDependencies, type MdeditMcpRuntime, type OpenArticleSession, type SharedCliConfig, type StreamableHttpOptionsFactory, type StreamableHttpRequest, createDurableMcpArticle, createMdeditMcpServer, createSharedOAuthCredentialProvider, createStreamableHttpHandler, optionsFromEnvironment, readSharedCliOAuthConfig, runStdioServer, runStdioServerFromEnvironment }; |
+610
-40
@@ -6,3 +6,3 @@ // src/index.ts | ||
| import { createRequire } from "module"; | ||
| import { randomUUID } from "crypto"; | ||
| import { createHash, randomUUID } from "crypto"; | ||
| import { | ||
@@ -16,2 +16,5 @@ normalizeApiBaseUrl, | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { | ||
| ListToolsRequestSchema | ||
| } from "@modelcontextprotocol/sdk/types.js"; | ||
| import { z } from "zod"; | ||
@@ -140,10 +143,258 @@ | ||
| }; | ||
| var articleIdentityOutputSchema = z.object({ | ||
| workspaceId: z.string(), | ||
| articleId: z.string() | ||
| }).catchall(z.unknown()); | ||
| var articleSummaryOutputSchema = z.object({ | ||
| articleId: z.string(), | ||
| workspaceId: z.string().optional(), | ||
| title: z.string().optional(), | ||
| folderId: z.string().nullable().optional(), | ||
| contentRevision: z.number().int().nonnegative().optional(), | ||
| contentHash: z.string().optional(), | ||
| collaborative: z.boolean().optional(), | ||
| createdAt: z.number().optional(), | ||
| isArchived: z.boolean().optional(), | ||
| isPinned: z.boolean().optional(), | ||
| updatedAt: z.number().optional() | ||
| }).catchall(z.unknown()); | ||
| var createArticleOutputSchema = z.object({ | ||
| workspaceId: z.string(), | ||
| articleId: z.string(), | ||
| title: z.string(), | ||
| content: z.string(), | ||
| contentHash: z.string(), | ||
| contentRevision: z.number().int().nonnegative(), | ||
| editorUrl: z.string().url() | ||
| }).catchall(z.unknown()); | ||
| var readArticleOutputSchema = articleSummaryOutputSchema.extend({ | ||
| workspaceId: z.string(), | ||
| content: z.string(), | ||
| contentRevision: z.number().int().nonnegative(), | ||
| contentHash: z.string() | ||
| }); | ||
| var publishOutputSchema = articleIdentityOutputSchema.extend({ | ||
| publishId: z.string(), | ||
| shortUrl: z.string().url(), | ||
| fullUrl: z.string().url(), | ||
| publishedAt: z.number() | ||
| }); | ||
| var publishStatusOutputSchema = articleIdentityOutputSchema.extend({ | ||
| isPublished: z.boolean(), | ||
| publishId: z.string().optional(), | ||
| shortUrl: z.string().url().optional(), | ||
| fullUrl: z.string().url().optional(), | ||
| publishedAt: z.number().optional(), | ||
| lastUpdated: z.number().optional(), | ||
| viewCount: z.number().nonnegative().optional(), | ||
| customSlug: z.string().optional(), | ||
| seoMetadata: seoMetadataSchema.optional() | ||
| }); | ||
| var unpublishOutputSchema = articleIdentityOutputSchema.extend({ | ||
| success: z.boolean(), | ||
| publishId: z.string() | ||
| }); | ||
| var reviewMutationOutputSchema = articleIdentityOutputSchema.extend({ | ||
| applied: z.boolean(), | ||
| commandId: z.string(), | ||
| events: z.array(z.unknown()) | ||
| }); | ||
| var MCP_TOOL_OUTPUT_SCHEMAS = { | ||
| list_articles: z.object({ articles: z.array(articleSummaryOutputSchema) }), | ||
| create_article: createArticleOutputSchema, | ||
| read_article: readArticleOutputSchema, | ||
| publish_article: publishOutputSchema, | ||
| get_publish_status: publishStatusOutputSchema, | ||
| unpublish_article: unpublishOutputSchema, | ||
| edit_article: articleIdentityOutputSchema.extend({ | ||
| mode: z.string(), | ||
| content: z.string(), | ||
| contentRevision: z.number().int().nonnegative(), | ||
| contentHash: z.string(), | ||
| result: z.object({ | ||
| applied: z.array(z.object({ | ||
| operationIndex: z.number().int().nonnegative(), | ||
| type: z.enum(["replace", "insert", "delete"]), | ||
| range: z.object({ | ||
| from: z.number().int().nonnegative(), | ||
| to: z.number().int().nonnegative() | ||
| }), | ||
| deletedText: z.string(), | ||
| insertedText: z.string() | ||
| })), | ||
| conflicts: z.array(z.unknown()) | ||
| }) | ||
| }), | ||
| add_comment: reviewMutationOutputSchema, | ||
| add_suggestion: reviewMutationOutputSchema, | ||
| reply_to_thread: reviewMutationOutputSchema, | ||
| resolve_thread: reviewMutationOutputSchema, | ||
| list_review_threads: articleIdentityOutputSchema.extend({ | ||
| items: z.array(z.unknown()) | ||
| }), | ||
| get_presence: articleIdentityOutputSchema.extend({ | ||
| mode: z.string(), | ||
| participants: z.array(z.unknown()) | ||
| }) | ||
| }; | ||
| var MDEDIT_MCP_TOOL_SCOPES = { | ||
| list_articles: ["articles:read"], | ||
| create_article: ["articles:write"], | ||
| read_article: ["articles:read"], | ||
| publish_article: ["publishing:write"], | ||
| get_publish_status: ["publishing:read"], | ||
| unpublish_article: ["publishing:write"], | ||
| edit_article: ["articles:write"], | ||
| add_comment: ["articles:read", "reviews:write"], | ||
| add_suggestion: ["articles:read", "reviews:write"], | ||
| reply_to_thread: ["articles:read", "reviews:write"], | ||
| resolve_thread: ["articles:read", "reviews:write"], | ||
| list_review_threads: ["articles:read", "reviews:read"], | ||
| get_presence: ["articles:read"] | ||
| }; | ||
| function oauthToolMetadata(toolName) { | ||
| return { | ||
| outputSchema: MCP_TOOL_OUTPUT_SCHEMAS[toolName], | ||
| _meta: { | ||
| securitySchemes: [{ | ||
| type: "oauth2", | ||
| scopes: [...MDEDIT_MCP_TOOL_SCOPES[toolName]] | ||
| }] | ||
| } | ||
| }; | ||
| } | ||
| function exposeOpenAiToolSecuritySchemes(server) { | ||
| const protocol = server.server; | ||
| const original = protocol._requestHandlers.get("tools/list"); | ||
| if (!original) throw new Error("MCP tools/list handler was not initialized"); | ||
| protocol.setRequestHandler(ListToolsRequestSchema, async (request, extra) => { | ||
| const result = await original(request, extra); | ||
| return { | ||
| ...result, | ||
| tools: (result.tools || []).map((tool) => ({ | ||
| ...tool, | ||
| // OpenAI's MCP contract currently consumes this extension at the tool | ||
| // top level. Keep the _meta mirror for standard SDK clients that strip | ||
| // unknown Tool fields while the MCP SDK catches up with the extension. | ||
| securitySchemes: tool._meta?.securitySchemes | ||
| })) | ||
| }; | ||
| }); | ||
| } | ||
| async function createDurableMcpArticle(input, { | ||
| openCollaborativeSession, | ||
| sdk | ||
| }) { | ||
| const articleId = randomUUID(); | ||
| const expectedContentHash = createHash("sha256").update(input.content).digest("hex"); | ||
| await sdk.article.create(input.workspaceId, { | ||
| articleId, | ||
| title: input.title, | ||
| folderId: input.folderId ?? null, | ||
| collaborative: input.collaborative | ||
| }); | ||
| try { | ||
| let committedContentRevision; | ||
| if (input.collaborative) { | ||
| const session = await openCollaborativeSession(input.workspaceId, articleId); | ||
| try { | ||
| const current = session.read(); | ||
| await session.edit([{ | ||
| type: "replace", | ||
| anchor: { range: { from: 0, to: current.length } }, | ||
| text: input.content | ||
| }]); | ||
| } finally { | ||
| await session.close(); | ||
| } | ||
| const checkpoint = await sdk.collaboration.checkpoint(input.workspaceId, articleId); | ||
| if (checkpoint.targetHashes["content.md"] !== expectedContentHash) { | ||
| throw Object.assign( | ||
| new Error("Collaborative document did not reach the expected durable checkpoint"), | ||
| { code: "MCP_CREATE_CHECKPOINT_MISMATCH" } | ||
| ); | ||
| } | ||
| } else { | ||
| const commit = await sdk.article.saveContent(input.workspaceId, articleId, input.content, { | ||
| baseContentRevision: 0, | ||
| contentHash: expectedContentHash, | ||
| folderId: input.folderId ?? null, | ||
| title: input.title | ||
| }); | ||
| committedContentRevision = commit?.contentRevision; | ||
| } | ||
| const saved = await sdk.article.get(input.workspaceId, articleId); | ||
| const savedContentRevision = typeof saved.contentRevision === "number" ? saved.contentRevision : void 0; | ||
| const contentRevision = savedContentRevision ?? committedContentRevision; | ||
| if (contentRevision === void 0) { | ||
| throw Object.assign( | ||
| new Error("Durable document did not return a content revision"), | ||
| { code: "MCP_CREATE_REVISION_MISSING" } | ||
| ); | ||
| } | ||
| return { | ||
| articleId, | ||
| content: saved.content ?? input.content, | ||
| contentHash: typeof saved.contentHash === "string" ? saved.contentHash : expectedContentHash, | ||
| contentRevision, | ||
| editorUrl: `https://app.mdedit.ai/workspaces/${encodeURIComponent(input.workspaceId)}/articles/${encodeURIComponent(articleId)}`, | ||
| title: saved.title ?? input.title, | ||
| workspaceId: input.workspaceId | ||
| }; | ||
| } catch (contentError) { | ||
| try { | ||
| await sdk.article.delete(input.workspaceId, articleId); | ||
| } catch { | ||
| throw Object.assign( | ||
| new Error("Document content save failed and the retained partial document could not be removed"), | ||
| { | ||
| code: "MCP_CREATE_PARTIAL_RETAINED", | ||
| details: { articleId, retainedPartialState: true } | ||
| } | ||
| ); | ||
| } | ||
| throw Object.assign( | ||
| new Error("Document content save failed; the partial document was removed"), | ||
| { | ||
| code: "MCP_CREATE_COMPENSATED", | ||
| cause: contentError, | ||
| details: { articleId, retainedPartialState: false } | ||
| } | ||
| ); | ||
| } | ||
| } | ||
| function defaultDependencies(options, telemetry) { | ||
| const apiUrl = options.apiUrl ?? DEFAULT_API_URL; | ||
| const credentialProvider = options.credentialProvider ?? { | ||
| getCredential: async () => { | ||
| const apiKey = options.apiKey?.trim(); | ||
| if (!apiKey) throw new TypeError("apiKey is required."); | ||
| return { | ||
| headers: { "x-api-key": apiKey }, | ||
| websocketToken: apiKey | ||
| }; | ||
| } | ||
| }; | ||
| const sdk = createSdkClient({ | ||
| baseUrlV2: normalizeApiBaseUrl(apiUrl), | ||
| getAuthHeaders: async () => ({ "x-api-key": options.apiKey }) | ||
| getAuthHeaders: async () => (await credentialProvider.getCredential()).headers, | ||
| signal: options.signal | ||
| }); | ||
| const agentName = options.agentName?.trim() || DEFAULT_AGENT_NAME; | ||
| const createArticle = (input) => createDurableMcpArticle( | ||
| input, | ||
| { | ||
| sdk, | ||
| openCollaborativeSession: (workspaceId, articleId) => openArticleSession({ | ||
| credentialProvider, | ||
| apiUrl, | ||
| workspaceId, | ||
| articleId, | ||
| agent: { name: agentName }, | ||
| signal: options.signal | ||
| }) | ||
| } | ||
| ); | ||
| return { | ||
| createArticle, | ||
| listArticles: (workspaceId) => sdk.article.list(workspaceId), | ||
@@ -155,3 +406,3 @@ readArticle: (workspaceId, articleId) => sdk.article.get(workspaceId, articleId), | ||
| openSession: (workspaceId, articleId) => openArticleSession({ | ||
| apiKey: options.apiKey, | ||
| credentialProvider, | ||
| apiUrl, | ||
@@ -161,2 +412,3 @@ workspaceId, | ||
| agent: { name: agentName }, | ||
| signal: options.signal, | ||
| telemetry: { | ||
@@ -200,10 +452,10 @@ capture: (event) => telemetry.capture({ | ||
| } = event; | ||
| const request = fetch(endpoint, { | ||
| const request = (async () => fetch(endpoint, { | ||
| method: "POST", | ||
| headers: { | ||
| "content-type": "application/json", | ||
| "x-api-key": options.apiKey | ||
| ...options.credentialProvider ? (await options.credentialProvider.getCredential()).headers : { "x-api-key": options.apiKey } | ||
| }, | ||
| body: JSON.stringify(payload) | ||
| }).catch(() => void 0); | ||
| }))().catch(() => void 0); | ||
| pending.add(request); | ||
@@ -232,5 +484,21 @@ void request.finally(() => pending.delete(request)); | ||
| if (article.updatedAt !== void 0) summary.updatedAt = article.updatedAt; | ||
| if (typeof article.contentRevision === "number") { | ||
| summary.contentRevision = article.contentRevision; | ||
| } | ||
| if (typeof article.contentHash === "string") summary.contentHash = article.contentHash; | ||
| if (typeof article.collaborative === "boolean") summary.collaborative = article.collaborative; | ||
| return summary; | ||
| } | ||
| function requiredContentMetadata(article, content) { | ||
| if (typeof article.contentRevision !== "number") { | ||
| throw Object.assign( | ||
| new Error("mdedit API did not return a content revision"), | ||
| { code: "MCP_CONTENT_REVISION_MISSING" } | ||
| ); | ||
| } | ||
| return { | ||
| contentHash: typeof article.contentHash === "string" ? article.contentHash : createHash("sha256").update(content).digest("hex"), | ||
| contentRevision: article.contentRevision | ||
| }; | ||
| } | ||
| function success(value) { | ||
@@ -244,3 +512,3 @@ return { | ||
| const rawMessage = error instanceof Error ? error.message : String(error); | ||
| const message = apiKey ? rawMessage.replaceAll(apiKey, "[redacted]") : rawMessage; | ||
| const message = (apiKey ? rawMessage.replaceAll(apiKey, "[redacted]") : rawMessage).replace(/\bBearer\s+\S+/gi, "Bearer [redacted]").replace(/\bmda1\.[A-Za-z0-9._-]+/g, "mda1.[redacted]").replace(/\bmdh_[A-Za-z0-9_-]+/g, "mdh_[redacted]").slice(0, 500); | ||
| return { | ||
@@ -251,3 +519,3 @@ isError: true, | ||
| } | ||
| async function callTool(apiKey, telemetry, toolName, operation) { | ||
| async function callTool(redactionSecret, telemetry, toolName, operation, transport, signal) { | ||
| const operationId = randomUUID(); | ||
@@ -260,3 +528,3 @@ const startedAt = Date.now(); | ||
| agent_source: "mcp", | ||
| transport: "stdio", | ||
| transport, | ||
| tool_name: toolName, | ||
@@ -267,3 +535,5 @@ retry_count: 0, | ||
| try { | ||
| signal?.throwIfAborted(); | ||
| const result = success(await operation()); | ||
| signal?.throwIfAborted(); | ||
| captureTelemetry(telemetry, { | ||
@@ -274,3 +544,3 @@ event: "mcp_tool_completed", | ||
| agent_source: "mcp", | ||
| transport: "stdio", | ||
| transport, | ||
| tool_name: toolName, | ||
@@ -288,3 +558,3 @@ duration_ms: Date.now() - startedAt, | ||
| agent_source: "mcp", | ||
| transport: "stdio", | ||
| transport, | ||
| tool_name: toolName, | ||
@@ -296,7 +566,10 @@ duration_ms: Date.now() - startedAt, | ||
| }); | ||
| return safeError(error, apiKey); | ||
| return safeError(error, redactionSecret ?? ""); | ||
| } | ||
| } | ||
| function createMdeditMcpServer(options) { | ||
| if (!options.apiKey?.trim()) throw new TypeError("apiKey is required."); | ||
| if (!options.apiKey?.trim() && !options.credentialProvider) { | ||
| throw new TypeError("apiKey or credentialProvider is required."); | ||
| } | ||
| const transport = options.transport ?? "stdio"; | ||
| const telemetry = options.telemetry ?? defaultTelemetry(options); | ||
@@ -319,10 +592,49 @@ const dependencies = options.dependencies ?? defaultDependencies(options, telemetry); | ||
| ); | ||
| const invokeTool = (toolName, operation) => { | ||
| const requiredScopes = MDEDIT_MCP_TOOL_SCOPES[toolName] || []; | ||
| const hasScope = (requiredScope) => options.availableScopes?.some( | ||
| (availableScope) => availableScope === requiredScope || requiredScope.endsWith(":read") && availableScope === `${requiredScope.slice(0, -5)}:write` | ||
| ); | ||
| const missingScopes = options.availableScopes ? requiredScopes.filter((scope) => !hasScope(scope)) : []; | ||
| if (missingScopes.length > 0) { | ||
| const qualifiedScopes = missingScopes.map( | ||
| (scope) => `https://mcp.mdedit.ai/mcp/${scope}` | ||
| ); | ||
| return Promise.resolve({ | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Additional OAuth scope required: ${missingScopes.join(", ")}` | ||
| }], | ||
| _meta: { | ||
| "mcp/www_authenticate": [ | ||
| [ | ||
| 'Bearer resource_metadata="https://mcp.mdedit.ai/.well-known/oauth-protected-resource"', | ||
| `scope="${qualifiedScopes.join(" ")}"`, | ||
| 'error="insufficient_scope"', | ||
| 'error_description="Additional permission is required for this tool"' | ||
| ].join(", ") | ||
| ] | ||
| } | ||
| }); | ||
| } | ||
| return callTool( | ||
| options.apiKey, | ||
| telemetry, | ||
| toolName, | ||
| operation, | ||
| transport, | ||
| options.signal | ||
| ); | ||
| }; | ||
| server.registerTool( | ||
| "list_articles", | ||
| { | ||
| title: "List Markdown Documents", | ||
| description: "List Markdown Documents in a workspace without returning their full content.", | ||
| inputSchema: z.object({ workspaceId: requiredId }).strict(), | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true } | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, | ||
| ...oauthToolMetadata("list_articles") | ||
| }, | ||
| ({ workspaceId }) => callTool(options.apiKey, telemetry, "list_articles", async () => ({ | ||
| ({ workspaceId }) => invokeTool("list_articles", async () => ({ | ||
| articles: (await dependencies.listArticles(workspaceId)).map(articleSummary) | ||
@@ -332,11 +644,56 @@ })) | ||
| server.registerTool( | ||
| "create_article", | ||
| { | ||
| title: "Create a Markdown Document", | ||
| description: "Create a durable Markdown Document with initial content in an accessible workspace.", | ||
| inputSchema: z.object({ | ||
| workspaceId: requiredId, | ||
| title: z.string().trim().min(1), | ||
| content: z.string(), | ||
| folderId: requiredId.optional(), | ||
| collaborative: z.boolean().optional().default(false) | ||
| }).strict(), | ||
| annotations: { | ||
| readOnlyHint: false, | ||
| destructiveHint: false, | ||
| idempotentHint: false, | ||
| openWorldHint: false | ||
| }, | ||
| ...oauthToolMetadata("create_article") | ||
| }, | ||
| ({ workspaceId, title, content, folderId, collaborative }) => invokeTool("create_article", async () => { | ||
| if (!dependencies.createArticle) { | ||
| throw Object.assign( | ||
| new Error("Document creation is unavailable in this MCP runtime"), | ||
| { code: "MCP_CREATE_UNAVAILABLE" } | ||
| ); | ||
| } | ||
| return dependencies.createArticle({ | ||
| workspaceId, | ||
| title, | ||
| content, | ||
| folderId, | ||
| collaborative | ||
| }); | ||
| }) | ||
| ); | ||
| server.registerTool( | ||
| "read_article", | ||
| { | ||
| title: "Read a Markdown Document", | ||
| description: "Read the current saved Markdown content and metadata for a document.", | ||
| inputSchema: articleInputSchema, | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true } | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, | ||
| ...oauthToolMetadata("read_article") | ||
| }, | ||
| ({ workspaceId, articleId }) => callTool(options.apiKey, telemetry, "read_article", async () => { | ||
| ({ workspaceId, articleId }) => invokeTool("read_article", async () => { | ||
| const article = await dependencies.readArticle(workspaceId, articleId); | ||
| return { ...articleSummary(article), workspaceId, articleId, content: article.content ?? "" }; | ||
| const content = article.content ?? ""; | ||
| return { | ||
| ...articleSummary(article), | ||
| workspaceId, | ||
| articleId, | ||
| content, | ||
| ...requiredContentMetadata(article, content) | ||
| }; | ||
| }) | ||
@@ -347,2 +704,3 @@ ); | ||
| { | ||
| title: "Publish a Markdown Document", | ||
| description: "Publish or update a Markdown Document at a public mded.it link. Requires publishing:write and explicit user confirmation.", | ||
@@ -354,5 +712,6 @@ inputSchema: articleInputSchema.extend({ | ||
| }).strict(), | ||
| annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false } | ||
| annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, | ||
| ...oauthToolMetadata("publish_article") | ||
| }, | ||
| ({ workspaceId, articleId, customSlug, seoMetadata }) => callTool(options.apiKey, telemetry, "publish_article", async () => ({ | ||
| ({ workspaceId, articleId, customSlug, seoMetadata }) => invokeTool("publish_article", async () => ({ | ||
| workspaceId, | ||
@@ -369,7 +728,9 @@ articleId, | ||
| { | ||
| title: "Get Markdown Document publish status", | ||
| description: "Get the public-link publishing status, URL, metadata, and view count for a Markdown Document.", | ||
| inputSchema: articleInputSchema, | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true } | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, | ||
| ...oauthToolMetadata("get_publish_status") | ||
| }, | ||
| ({ workspaceId, articleId }) => callTool(options.apiKey, telemetry, "get_publish_status", async () => ({ | ||
| ({ workspaceId, articleId }) => invokeTool("get_publish_status", async () => ({ | ||
| workspaceId, | ||
@@ -383,2 +744,3 @@ articleId, | ||
| { | ||
| title: "Unpublish a Markdown Document", | ||
| description: "Disable a Markdown Document public link. Requires publishing:write and explicit user confirmation.", | ||
@@ -388,5 +750,6 @@ inputSchema: articleInputSchema.extend({ | ||
| }).strict(), | ||
| annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false } | ||
| annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, | ||
| ...oauthToolMetadata("unpublish_article") | ||
| }, | ||
| ({ workspaceId, articleId }) => callTool(options.apiKey, telemetry, "unpublish_article", async () => ({ | ||
| ({ workspaceId, articleId }) => invokeTool("unpublish_article", async () => ({ | ||
| workspaceId, | ||
@@ -400,2 +763,3 @@ articleId, | ||
| { | ||
| title: "Edit a Markdown Document", | ||
| description: "Apply anchored edits to a Markdown Document through a persistent live session when collaboration is enabled.", | ||
@@ -406,7 +770,18 @@ inputSchema: articleInputSchema.extend({ | ||
| }).strict(), | ||
| annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false } | ||
| annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, | ||
| ...oauthToolMetadata("edit_article") | ||
| }, | ||
| ({ workspaceId, articleId, operations, ifContentHash }) => callTool(options.apiKey, telemetry, "edit_article", () => pool.use(workspaceId, articleId, async (session) => { | ||
| ({ workspaceId, articleId, operations, ifContentHash }) => invokeTool("edit_article", () => pool.use(workspaceId, articleId, async (session) => { | ||
| const result = await session.edit(operations, { ifContentHash }); | ||
| return { workspaceId, articleId, mode: session.mode, content: session.read(), result }; | ||
| const sessionContent = session.read(); | ||
| const saved = await dependencies.readArticle(workspaceId, articleId); | ||
| const content = saved.content ?? sessionContent; | ||
| return { | ||
| workspaceId, | ||
| articleId, | ||
| mode: session.mode, | ||
| content, | ||
| ...requiredContentMetadata(saved, content), | ||
| result | ||
| }; | ||
| })) | ||
@@ -417,2 +792,3 @@ ); | ||
| { | ||
| title: "Add a review comment", | ||
| description: "Add an anchored review comment without directly changing document prose.", | ||
@@ -424,5 +800,6 @@ inputSchema: articleInputSchema.extend({ | ||
| }).strict(), | ||
| annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false } | ||
| annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, | ||
| ...oauthToolMetadata("add_comment") | ||
| }, | ||
| ({ workspaceId, articleId, anchor, body, targetId, commandId }) => callTool(options.apiKey, telemetry, "add_comment", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| ({ workspaceId, articleId, anchor, body, targetId, commandId }) => invokeTool("add_comment", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| workspaceId, | ||
@@ -436,2 +813,3 @@ articleId, | ||
| { | ||
| title: "Add a review suggestion", | ||
| description: "Add an anchored replacement suggestion for a human to accept or reject.", | ||
@@ -444,5 +822,6 @@ inputSchema: articleInputSchema.extend({ | ||
| }).strict(), | ||
| annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false } | ||
| annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, | ||
| ...oauthToolMetadata("add_suggestion") | ||
| }, | ||
| ({ workspaceId, articleId, anchor, replace, note, targetId, commandId }) => callTool(options.apiKey, telemetry, "add_suggestion", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| ({ workspaceId, articleId, anchor, replace, note, targetId, commandId }) => invokeTool("add_suggestion", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| workspaceId, | ||
@@ -456,2 +835,3 @@ articleId, | ||
| { | ||
| title: "Reply to a review thread", | ||
| description: "Reply to an existing review thread.", | ||
@@ -463,5 +843,6 @@ inputSchema: articleInputSchema.extend({ | ||
| }).strict(), | ||
| annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false } | ||
| annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, | ||
| ...oauthToolMetadata("reply_to_thread") | ||
| }, | ||
| ({ workspaceId, articleId, threadId, body, targetId, commandId }) => callTool(options.apiKey, telemetry, "reply_to_thread", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| ({ workspaceId, articleId, threadId, body, targetId, commandId }) => invokeTool("reply_to_thread", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| workspaceId, | ||
@@ -475,2 +856,3 @@ articleId, | ||
| { | ||
| title: "Resolve a review thread", | ||
| description: "Resolve an existing review thread.", | ||
@@ -481,5 +863,6 @@ inputSchema: articleInputSchema.extend({ | ||
| }).strict(), | ||
| annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false } | ||
| annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, | ||
| ...oauthToolMetadata("resolve_thread") | ||
| }, | ||
| ({ workspaceId, articleId, threadId, targetId, commandId }) => callTool(options.apiKey, telemetry, "resolve_thread", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| ({ workspaceId, articleId, threadId, targetId, commandId }) => invokeTool("resolve_thread", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| workspaceId, | ||
@@ -493,2 +876,3 @@ articleId, | ||
| { | ||
| title: "List review threads", | ||
| description: "List review comments, highlights, and suggestions for a document.", | ||
@@ -499,5 +883,6 @@ inputSchema: articleInputSchema.extend({ | ||
| }).strict(), | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true } | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, | ||
| ...oauthToolMetadata("list_review_threads") | ||
| }, | ||
| ({ workspaceId, articleId, targetId, status }) => callTool(options.apiKey, telemetry, "list_review_threads", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| ({ workspaceId, articleId, targetId, status }) => invokeTool("list_review_threads", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| workspaceId, | ||
@@ -511,7 +896,9 @@ articleId, | ||
| { | ||
| title: "Get document presence", | ||
| description: "List the people and agents currently present in a collaborative document.", | ||
| inputSchema: articleInputSchema, | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true } | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, | ||
| ...oauthToolMetadata("get_presence") | ||
| }, | ||
| ({ workspaceId, articleId }) => callTool(options.apiKey, telemetry, "get_presence", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| ({ workspaceId, articleId }) => invokeTool("get_presence", () => pool.use(workspaceId, articleId, async (session) => ({ | ||
| workspaceId, | ||
@@ -523,2 +910,3 @@ articleId, | ||
| ); | ||
| exposeOpenAiToolSecuritySchemes(server); | ||
| return { | ||
@@ -534,2 +922,173 @@ server, | ||
| // src/oauthCredentialProvider.ts | ||
| import { existsSync, readFileSync } from "fs"; | ||
| import { homedir } from "os"; | ||
| import { join } from "path"; | ||
| import { | ||
| NodeOAuthClient, | ||
| OsCredentialStore | ||
| } from "@mdedit/node-auth"; | ||
| var API_RESOURCE = "https://apiv2.mdedit.ai/api"; | ||
| function configPath(environment) { | ||
| return environment.MDEDIT_CONFIG_FILE || join( | ||
| environment.MDEDIT_CONFIG_DIR || join(homedir(), ".mdedit-cli"), | ||
| "config.json" | ||
| ); | ||
| } | ||
| function readSharedCliOAuthConfig(environment) { | ||
| const path = configPath(environment); | ||
| if (!existsSync(path)) return {}; | ||
| try { | ||
| const parsed = JSON.parse(readFileSync(path, "utf8")); | ||
| return { | ||
| apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : void 0, | ||
| oauthProfile: typeof parsed.oauthProfile === "string" ? parsed.oauthProfile : void 0, | ||
| oauthProfiles: Array.isArray(parsed.oauthProfiles) ? parsed.oauthProfiles.filter((value) => typeof value === "string") : void 0 | ||
| }; | ||
| } catch { | ||
| throw new Error(`Shared mdedit OAuth config is invalid: ${path}`); | ||
| } | ||
| } | ||
| function apiRoot(value) { | ||
| const normalized = value.replace(/\/+$/, ""); | ||
| if (normalized.endsWith("/api/v2")) return normalized.slice(0, -7); | ||
| if (normalized.endsWith("/api")) return normalized.slice(0, -4); | ||
| return normalized; | ||
| } | ||
| async function createSharedOAuthCredentialProvider({ | ||
| credentialStore, | ||
| environment = process.env, | ||
| fetch: fetch2 = globalThis.fetch, | ||
| sharedConfig | ||
| } = {}) { | ||
| const config = sharedConfig || readSharedCliOAuthConfig(environment); | ||
| const profile = environment.MDEDIT_PROFILE?.trim() || config.oauthProfile?.trim() || "default"; | ||
| if (!config.oauthProfiles?.includes(profile)) { | ||
| throw new Error( | ||
| `OAuth profile "${profile}" is not logged in. Run \`mdedit auth login${profile === "default" ? "" : ` --profile ${profile}`}\`.` | ||
| ); | ||
| } | ||
| const root = apiRoot( | ||
| environment.MDEDIT_API_URL?.trim() || config.apiUrl?.trim() || "https://apiv2.mdedit.ai" | ||
| ); | ||
| const discoveryResponse = await fetch2(`${root}/.well-known/mdedit-cli-oauth`); | ||
| if (!discoveryResponse.ok) { | ||
| throw new Error(`mdedit OAuth discovery failed (${discoveryResponse.status})`); | ||
| } | ||
| const discovery = await discoveryResponse.json(); | ||
| if (!discovery.clientId || !discovery.authorizationEndpoint || !discovery.tokenEndpoint || discovery.resource !== API_RESOURCE) { | ||
| throw new Error("mdedit OAuth discovery is incomplete or has the wrong resource"); | ||
| } | ||
| const store = credentialStore || new OsCredentialStore({ | ||
| loadProfileIndex: async () => config.oauthProfiles || [], | ||
| saveProfileIndex: async () => void 0 | ||
| }); | ||
| const oauthConfiguration = { | ||
| authorizationEndpoint: discovery.authorizationEndpoint, | ||
| clientId: discovery.clientId, | ||
| redirectUri: "http://localhost:17643/oauth/callback", | ||
| resource: discovery.resource, | ||
| revocationEndpoint: discovery.revocationEndpoint, | ||
| scopes: discovery.defaultScopes, | ||
| tokenEndpoint: discovery.tokenEndpoint | ||
| }; | ||
| const client = new NodeOAuthClient({ | ||
| configuration: oauthConfiguration, | ||
| credentialStore: store, | ||
| fetch: fetch2 | ||
| }); | ||
| const tokenSet = await client.status(profile); | ||
| if (!tokenSet.authenticated) { | ||
| throw new Error(`OAuth profile "${profile}" is unavailable. Run \`mdedit auth login\`.`); | ||
| } | ||
| const scopePrefix = `${API_RESOURCE}/`; | ||
| const availableScopes = (tokenSet.scope || []).map((scope) => scope.startsWith(scopePrefix) ? scope.slice(scopePrefix.length) : scope).filter(Boolean); | ||
| const credential = async (forceRefresh) => { | ||
| const accessToken = await client.getAccessToken(profile, { forceRefresh }); | ||
| return { | ||
| headers: { Authorization: `Bearer ${accessToken}` }, | ||
| websocketToken: accessToken | ||
| }; | ||
| }; | ||
| return { | ||
| availableScopes, | ||
| credentialProvider: { | ||
| getCredential: () => credential(false), | ||
| refreshCredential: () => credential(true) | ||
| } | ||
| }; | ||
| } | ||
| // src/http.ts | ||
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| function jsonError(response, status, message) { | ||
| response.writeHead(status, { | ||
| "content-type": "application/json", | ||
| "cache-control": "no-store" | ||
| }); | ||
| response.end(JSON.stringify({ | ||
| jsonrpc: "2.0", | ||
| error: { code: -32e3, message }, | ||
| id: null | ||
| })); | ||
| } | ||
| function createStreamableHttpHandler({ | ||
| optionsForRequest | ||
| }) { | ||
| return async (request, response) => { | ||
| if (request.headers.origin) { | ||
| jsonError(response, 403, "Browser-origin MCP requests are not allowed"); | ||
| return; | ||
| } | ||
| if (request.headers["mcp-session-id"]) { | ||
| jsonError(response, 400, "Mcp-Session-Id is not accepted by the stateless endpoint"); | ||
| return; | ||
| } | ||
| if (request.method !== "POST") { | ||
| response.setHeader("allow", "POST"); | ||
| jsonError(response, 405, "Method not allowed"); | ||
| return; | ||
| } | ||
| const requestAbort = new AbortController(); | ||
| const onRequestAborted = () => requestAbort.abort( | ||
| Object.assign(new Error("MCP HTTP request was cancelled"), { | ||
| code: "MCP_REQUEST_CANCELLED" | ||
| }) | ||
| ); | ||
| const onResponseClosed = () => { | ||
| if (!response.writableFinished) onRequestAborted(); | ||
| }; | ||
| request.once("aborted", onRequestAborted); | ||
| response.once("close", onResponseClosed); | ||
| try { | ||
| const requestOptions = await optionsForRequest(request, requestAbort.signal); | ||
| let runtime; | ||
| let transport; | ||
| try { | ||
| runtime = createMdeditMcpServer({ | ||
| ...requestOptions, | ||
| signal: requestAbort.signal, | ||
| transport: "streamable-http" | ||
| }); | ||
| transport = new StreamableHTTPServerTransport({ | ||
| enableJsonResponse: true, | ||
| sessionIdGenerator: void 0 | ||
| }); | ||
| await runtime.server.connect(transport); | ||
| await transport.handleRequest(request, response, request.body); | ||
| } catch { | ||
| if (!response.headersSent && !requestAbort.signal.aborted) { | ||
| jsonError(response, 500, "Internal MCP request failure"); | ||
| } | ||
| } finally { | ||
| await transport?.close().catch(() => void 0); | ||
| await runtime?.close().catch(() => void 0); | ||
| } | ||
| } finally { | ||
| request.removeListener("aborted", onRequestAborted); | ||
| response.removeListener("close", onResponseClosed); | ||
| } | ||
| }; | ||
| } | ||
| // src/index.ts | ||
@@ -566,8 +1125,19 @@ function optionsFromEnvironment(environment = process.env) { | ||
| function runStdioServerFromEnvironment(environment = process.env) { | ||
| return runStdioServer(optionsFromEnvironment(environment)); | ||
| const apiKey = environment.MDEDIT_API_KEY?.trim(); | ||
| if (apiKey) return runStdioServer(optionsFromEnvironment(environment)); | ||
| return createSharedOAuthCredentialProvider({ environment }).then((oauth) => runStdioServer({ | ||
| ...oauth, | ||
| ...environment.MDEDIT_API_URL?.trim() ? { apiUrl: environment.MDEDIT_API_URL.trim() } : {}, | ||
| ...environment.MDEDIT_AGENT_NAME?.trim() ? { agentName: environment.MDEDIT_AGENT_NAME.trim() } : {} | ||
| })); | ||
| } | ||
| export { | ||
| ArticleSessionPool, | ||
| MDEDIT_MCP_TOOL_SCOPES, | ||
| createDurableMcpArticle, | ||
| createMdeditMcpServer, | ||
| createSharedOAuthCredentialProvider, | ||
| createStreamableHttpHandler, | ||
| optionsFromEnvironment, | ||
| readSharedCliOAuthConfig, | ||
| runStdioServer, | ||
@@ -574,0 +1144,0 @@ runStdioServerFromEnvironment |
+7
-3
| { | ||
| "name": "@mdedit/mcp-server", | ||
| "version": "0.2.0", | ||
| "version": "0.2.1", | ||
| "private": false, | ||
@@ -16,2 +16,3 @@ "type": "module", | ||
| "license": "Apache-2.0", | ||
| "mcpName": "io.github.mangoappstudio/mdedit", | ||
| "publishConfig": { | ||
@@ -45,7 +46,9 @@ "access": "public" | ||
| "smoke:claude": "yarn --cwd ../.. workspace @mdedit/agent-client build && yarn build && node scripts/claude-smoke.mjs", | ||
| "registry:validate": "node scripts/validate-server-json.mjs", | ||
| "prepack": "yarn build" | ||
| }, | ||
| "dependencies": { | ||
| "@mdedit/agent-client": "0.2.0", | ||
| "@mdedit/sdk": "0.3.0", | ||
| "@mdedit/agent-client": "1.0.0", | ||
| "@mdedit/node-auth": "0.1.0", | ||
| "@mdedit/sdk": "0.3.1", | ||
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
@@ -55,2 +58,3 @@ "zod": "^4.4.3" | ||
| "devDependencies": { | ||
| "ajv": "^6.12.6", | ||
| "@types/node": "^20.10.5", | ||
@@ -57,0 +61,0 @@ "tsup": "^8.0.2", |
+16
-4
| # @mdedit/mcp-server | ||
| Use mdedit.ai Markdown Documents from Claude Code and other Model Context Protocol clients. The server runs locally over stdio and authenticates to mdedit with a scoped API key. | ||
| Use mdedit.ai Markdown Documents from Claude Code and other Model Context | ||
| Protocol clients. The server runs locally over stdio and authenticates with a | ||
| scoped API key. It can also share an OS-keychain OAuth profile created by | ||
| `mdedit auth login` in environments where the release-gated OAuth endpoint has | ||
| been enabled. | ||
| ## Claude Code setup | ||
| Create an API key in mdedit, then add the server: | ||
| The generally available setup uses a scoped API key: | ||
@@ -17,2 +21,5 @@ ```bash | ||
| When OAuth is enabled for your environment, `mdedit auth login` may be used | ||
| before the same `claude mcp add` command without the API-key environment value. | ||
| Reviewer agents normally use `articles:read,reviews:write`. Editing agents need `articles:write`. Accepting a suggestion also requires `articles:write` because it changes document content. | ||
@@ -25,2 +32,3 @@ Agents that publish public links need `publishing:write`; publication status only needs | ||
| - `list_articles` | ||
| - `create_article` | ||
| - `read_article` | ||
@@ -41,7 +49,11 @@ - `publish_article` | ||
| Live collaborative documents keep one agent session open across tool calls. The default idle timeout is 60 seconds; non-collaborative documents use the REST fallback and report an empty live-presence list. | ||
| Local stdio keeps one live collaborative session open across tool calls. The | ||
| hosted HTTP adapter uses operation-scoped sessions and does not depend on | ||
| process-local state or load-balancer stickiness. Non-collaborative documents use | ||
| the REST fallback and report an empty live-presence list. | ||
| ## Environment | ||
| - `MDEDIT_API_KEY` — required mdedit API key | ||
| - `MDEDIT_API_KEY` - optional scoped API key override; takes precedence over OAuth | ||
| - `MDEDIT_PROFILE` - optional shared OAuth profile name; defaults to the active CLI profile | ||
| - `MDEDIT_API_URL` — optional API host override | ||
@@ -48,0 +60,0 @@ - `MDEDIT_AGENT_NAME` — optional awareness display hint; defaults to `mdedit-mcp` |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
130139
98.98%1273
107.67%73
19.67%5
25%5
25%7
40%8
700%+ Added
+ Added
+ Added
- Removed
- Removed
Updated
Updated