@mdedit/mcp-server
Advanced tools
+19
-3
| import { ArticleSession, AgentCredentialProvider } from '@mdedit/agent-client'; | ||
| import { IArticle, PublishArticleRequest, PublishArticleResponse, PublishStatusResponse, UnpublishArticleResponse } from '@mdedit/sdk'; | ||
| import { IWorkspace, IArticle, PublishArticleRequest, PublishArticleResponse, PublishStatusResponse, UnpublishArticleResponse } from '@mdedit/sdk'; | ||
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
| import { IncomingMessage, ServerResponse } from 'node:http'; | ||
| import { OAuthCredentialStore } from '@mdedit/node-auth'; | ||
@@ -24,2 +23,3 @@ type OpenArticleSession = (workspaceId: string, articleId: string) => Promise<ArticleSession>; | ||
| declare const MDEDIT_MCP_TOOL_SCOPES: { | ||
| readonly list_workspaces: readonly ["workspaces:read"]; | ||
| readonly list_articles: readonly ["articles:read"]; | ||
@@ -39,2 +39,3 @@ readonly create_article: readonly ["articles:write"]; | ||
| }; | ||
| declare const MDEDIT_MCP_OAUTH_RESOURCE = "https://mcp.mdedit.ai/mcp"; | ||
| interface CreateMcpArticleInput { | ||
@@ -58,2 +59,3 @@ collaborative: boolean; | ||
| createArticle?(input: CreateMcpArticleInput): Promise<CreateMcpArticleResult>; | ||
| listWorkspaces?(): Promise<IWorkspace[]>; | ||
| listArticles(workspaceId: string): Promise<IArticle[]>; | ||
@@ -139,2 +141,16 @@ readArticle(workspaceId: string, articleId: string): Promise<IArticle>; | ||
| interface OAuthTokenSet { | ||
| accessToken: string; | ||
| expiresAt: number; | ||
| refreshToken: string; | ||
| scope: string[]; | ||
| tokenType: string; | ||
| } | ||
| interface OAuthCredentialStore { | ||
| delete(profile: string): Promise<void>; | ||
| get(profile: string): Promise<OAuthTokenSet | null>; | ||
| listProfiles(): Promise<string[]>; | ||
| set(profile: string, tokenSet: OAuthTokenSet): Promise<void>; | ||
| } | ||
| interface SharedCliConfig { | ||
@@ -161,2 +177,2 @@ apiUrl?: string; | ||
| 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 }; | ||
| export { ArticleSessionPool, type ArticleSessionPoolOptions, type CreateMcpArticleInput, type CreateMcpArticleResult, type CreateMdeditMcpServerOptions, MDEDIT_MCP_OAUTH_RESOURCE, 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 }; |
+307
-8
@@ -145,2 +145,9 @@ // src/index.ts | ||
| }).catchall(z.unknown()); | ||
| var workspaceSummaryOutputSchema = z.object({ | ||
| workspaceId: z.string(), | ||
| name: z.string(), | ||
| type: z.enum(["user", "team"]).optional(), | ||
| createdAt: z.number().optional(), | ||
| updatedAt: z.number().optional() | ||
| }).catchall(z.unknown()); | ||
| var articleSummaryOutputSchema = z.object({ | ||
@@ -201,2 +208,3 @@ articleId: z.string(), | ||
| var MCP_TOOL_OUTPUT_SCHEMAS = { | ||
| list_workspaces: z.object({ workspaces: z.array(workspaceSummaryOutputSchema) }), | ||
| list_articles: z.object({ articles: z.array(articleSummaryOutputSchema) }), | ||
@@ -240,2 +248,3 @@ create_article: createArticleOutputSchema, | ||
| var MDEDIT_MCP_TOOL_SCOPES = { | ||
| list_workspaces: ["workspaces:read"], | ||
| list_articles: ["articles:read"], | ||
@@ -255,2 +264,3 @@ create_article: ["articles:write"], | ||
| }; | ||
| var MDEDIT_MCP_OAUTH_RESOURCE = "https://mcp.mdedit.ai/mcp"; | ||
| function oauthToolMetadata(toolName) { | ||
@@ -262,3 +272,8 @@ return { | ||
| type: "oauth2", | ||
| scopes: [...MDEDIT_MCP_TOOL_SCOPES[toolName]] | ||
| // Cognito custom scopes are identified by the resource server URI plus | ||
| // the operation scope. OpenAI requests these values verbatim, so the | ||
| // tool contract must advertise the qualified form accepted by Cognito. | ||
| scopes: MDEDIT_MCP_TOOL_SCOPES[toolName].map( | ||
| (scope) => `${MDEDIT_MCP_OAUTH_RESOURCE}/${scope}` | ||
| ) | ||
| }] | ||
@@ -402,2 +417,3 @@ } | ||
| createArticle, | ||
| listWorkspaces: () => sdk.workspace.list(), | ||
| listArticles: (workspaceId) => sdk.article.list(workspaceId), | ||
@@ -475,2 +491,12 @@ readArticle: (workspaceId, articleId) => sdk.article.get(workspaceId, articleId), | ||
| } | ||
| function normalizeOutputTimestamp(value) { | ||
| if (typeof value === "number") { | ||
| return Number.isFinite(value) ? value : void 0; | ||
| } | ||
| if (typeof value !== "string" || value.trim() === "") return void 0; | ||
| const numeric = Number(value); | ||
| if (Number.isFinite(numeric)) return numeric; | ||
| const parsed = Date.parse(value); | ||
| return Number.isFinite(parsed) ? parsed : void 0; | ||
| } | ||
| function articleSummary(article) { | ||
@@ -483,4 +509,6 @@ const summary = { articleId: article.articleId }; | ||
| if (article.isPinned !== void 0) summary.isPinned = article.isPinned; | ||
| if (article.createdAt !== void 0) summary.createdAt = article.createdAt; | ||
| if (article.updatedAt !== void 0) summary.updatedAt = article.updatedAt; | ||
| const createdAt = normalizeOutputTimestamp(article.createdAt); | ||
| const updatedAt = normalizeOutputTimestamp(article.updatedAt); | ||
| if (createdAt !== void 0) summary.createdAt = createdAt; | ||
| if (updatedAt !== void 0) summary.updatedAt = updatedAt; | ||
| if (typeof article.contentRevision === "number") { | ||
@@ -493,2 +521,14 @@ summary.contentRevision = article.contentRevision; | ||
| } | ||
| function workspaceSummary(workspace) { | ||
| const summary = { | ||
| workspaceId: workspace.workspaceId, | ||
| name: workspace.name | ||
| }; | ||
| if (workspace.type !== void 0) summary.type = workspace.type; | ||
| const createdAt = normalizeOutputTimestamp(workspace.createdAt); | ||
| const updatedAt = normalizeOutputTimestamp(workspace.updatedAt); | ||
| if (createdAt !== void 0) summary.createdAt = createdAt; | ||
| if (updatedAt !== void 0) summary.updatedAt = updatedAt; | ||
| return summary; | ||
| } | ||
| function requiredContentMetadata(article, content) { | ||
@@ -580,3 +620,3 @@ if (typeof article.contentRevision !== "number") { | ||
| instructions: [ | ||
| "Use list_articles before choosing a document.", | ||
| "Use list_workspaces before choosing a workspace, then list_articles before choosing a document.", | ||
| "Prefer add_suggestion for proposed content changes that need human approval.", | ||
@@ -626,2 +666,23 @@ "Use edit_article only when the API key has articles:write scope.", | ||
| server.registerTool( | ||
| "list_workspaces", | ||
| { | ||
| title: "List mdedit Workspaces", | ||
| description: "List the mdedit workspaces available to the signed-in user without returning document content.", | ||
| inputSchema: z.object({}).strict(), | ||
| annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, | ||
| ...oauthToolMetadata("list_workspaces") | ||
| }, | ||
| () => invokeTool("list_workspaces", async () => { | ||
| if (!dependencies.listWorkspaces) { | ||
| throw Object.assign( | ||
| new Error("Workspace discovery is unavailable in this MCP runtime"), | ||
| { code: "MCP_WORKSPACE_DISCOVERY_UNAVAILABLE" } | ||
| ); | ||
| } | ||
| return { | ||
| workspaces: (await dependencies.listWorkspaces()).map(workspaceSummary) | ||
| }; | ||
| }) | ||
| ); | ||
| server.registerTool( | ||
| "list_articles", | ||
@@ -902,6 +963,243 @@ { | ||
| import { join } from "path"; | ||
| import { | ||
| NodeOAuthClient, | ||
| OsCredentialStore | ||
| } from "@mdedit/node-auth"; | ||
| // ../node-auth/dist/index.js | ||
| import { Entry } from "@napi-rs/keyring"; | ||
| var OAuthClientError = class extends Error { | ||
| constructor(message, code, details) { | ||
| super(message); | ||
| this.code = code; | ||
| this.details = details; | ||
| this.name = "OAuthClientError"; | ||
| } | ||
| }; | ||
| var refreshesByCredentialStore = /* @__PURE__ */ new WeakMap(); | ||
| function required(value, name) { | ||
| const normalized = value?.trim(); | ||
| if (!normalized) throw new TypeError(`${name} is required`); | ||
| return normalized; | ||
| } | ||
| async function responsePayload(response) { | ||
| const payload = await response.json().catch(() => ({})); | ||
| return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {}; | ||
| } | ||
| function stringField(payload, name) { | ||
| const value = payload[name]; | ||
| return typeof value === "string" ? value : ""; | ||
| } | ||
| var NodeOAuthClient = class { | ||
| configuration; | ||
| credentialStore; | ||
| fetch; | ||
| refreshes; | ||
| constructor({ | ||
| configuration, | ||
| credentialStore, | ||
| fetch: fetch2 = globalThis.fetch | ||
| }) { | ||
| this.configuration = configuration; | ||
| this.credentialStore = credentialStore; | ||
| this.fetch = fetch2; | ||
| const existingRefreshes = refreshesByCredentialStore.get(credentialStore); | ||
| this.refreshes = existingRefreshes ?? /* @__PURE__ */ new Map(); | ||
| if (!existingRefreshes) { | ||
| refreshesByCredentialStore.set(credentialStore, this.refreshes); | ||
| } | ||
| } | ||
| createAuthorizationRequest({ | ||
| challenge, | ||
| state | ||
| }) { | ||
| const url = new URL(this.configuration.authorizationEndpoint); | ||
| url.search = new URLSearchParams({ | ||
| client_id: required(this.configuration.clientId, "clientId"), | ||
| code_challenge: required(challenge, "challenge"), | ||
| code_challenge_method: "S256", | ||
| redirect_uri: required(this.configuration.redirectUri, "redirectUri"), | ||
| resource: required(this.configuration.resource, "resource"), | ||
| response_type: "code", | ||
| scope: this.configuration.scopes.join(" "), | ||
| state: required(state, "state") | ||
| }).toString(); | ||
| return { url: url.toString() }; | ||
| } | ||
| async exchangeAuthorizationCode({ | ||
| code, | ||
| profile, | ||
| verifier | ||
| }) { | ||
| const tokenSet = await this.tokenRequest(new URLSearchParams({ | ||
| client_id: this.configuration.clientId, | ||
| code: required(code, "code"), | ||
| code_verifier: required(verifier, "verifier"), | ||
| grant_type: "authorization_code", | ||
| redirect_uri: this.configuration.redirectUri, | ||
| resource: this.configuration.resource | ||
| }), "OAUTH_CODE_EXCHANGE_FAILED"); | ||
| await this.credentialStore.set(required(profile, "profile"), tokenSet); | ||
| return tokenSet; | ||
| } | ||
| async getAccessToken(profile, { forceRefresh = false } = {}) { | ||
| const normalizedProfile = required(profile, "profile"); | ||
| const tokenSet = await this.credentialStore.get(normalizedProfile); | ||
| if (!tokenSet) { | ||
| throw new OAuthClientError( | ||
| `OAuth profile "${normalizedProfile}" is not logged in`, | ||
| "OAUTH_LOGIN_REQUIRED" | ||
| ); | ||
| } | ||
| if (!forceRefresh && tokenSet.expiresAt > Date.now() + 3e4) { | ||
| return tokenSet.accessToken; | ||
| } | ||
| const refreshKey = JSON.stringify([ | ||
| this.configuration.tokenEndpoint, | ||
| this.configuration.clientId, | ||
| this.configuration.resource, | ||
| normalizedProfile | ||
| ]); | ||
| let refresh = this.refreshes.get(refreshKey); | ||
| if (!refresh) { | ||
| refresh = this.refresh(normalizedProfile, tokenSet).finally(() => this.refreshes.delete(refreshKey)); | ||
| this.refreshes.set(refreshKey, refresh); | ||
| } | ||
| return (await refresh).accessToken; | ||
| } | ||
| async status(profile) { | ||
| const normalizedProfile = required(profile, "profile"); | ||
| const tokenSet = await this.credentialStore.get(normalizedProfile); | ||
| return tokenSet ? { | ||
| authenticated: true, | ||
| expiresAt: tokenSet.expiresAt, | ||
| profile: normalizedProfile, | ||
| scope: [...tokenSet.scope] | ||
| } : { authenticated: false, profile: normalizedProfile }; | ||
| } | ||
| async logout(profile) { | ||
| const normalizedProfile = required(profile, "profile"); | ||
| const tokenSet = await this.credentialStore.get(normalizedProfile); | ||
| if (!tokenSet) return; | ||
| await this.revoke(tokenSet.refreshToken); | ||
| await this.credentialStore.delete(normalizedProfile); | ||
| } | ||
| async logoutAll() { | ||
| for (const profile of await this.credentialStore.listProfiles()) { | ||
| await this.logout(profile); | ||
| } | ||
| } | ||
| async refresh(profile, current) { | ||
| const replacement = await this.tokenRequest(new URLSearchParams({ | ||
| client_id: this.configuration.clientId, | ||
| grant_type: "refresh_token", | ||
| refresh_token: current.refreshToken, | ||
| resource: this.configuration.resource | ||
| }), "OAUTH_REFRESH_FAILED", { | ||
| refreshToken: current.refreshToken, | ||
| scopes: current.scope | ||
| }); | ||
| await this.credentialStore.set(profile, replacement); | ||
| return replacement; | ||
| } | ||
| async revoke(refreshToken) { | ||
| if (!this.configuration.revocationEndpoint) { | ||
| throw new OAuthClientError( | ||
| "OAuth revocation endpoint is not configured", | ||
| "OAUTH_REVOCATION_UNAVAILABLE" | ||
| ); | ||
| } | ||
| const response = await this.fetch(this.configuration.revocationEndpoint, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/x-www-form-urlencoded" }, | ||
| body: new URLSearchParams({ | ||
| client_id: this.configuration.clientId, | ||
| token: refreshToken | ||
| }) | ||
| }); | ||
| if (!response.ok) { | ||
| throw new OAuthClientError( | ||
| `OAuth revocation failed with status ${response.status}`, | ||
| "OAUTH_REVOCATION_FAILED", | ||
| { status: response.status } | ||
| ); | ||
| } | ||
| } | ||
| async tokenRequest(body, errorCode, fallback) { | ||
| const response = await this.fetch(this.configuration.tokenEndpoint, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/x-www-form-urlencoded" }, | ||
| body | ||
| }); | ||
| const payload = await responsePayload(response); | ||
| if (!response.ok) { | ||
| throw new OAuthClientError( | ||
| `OAuth token request failed with status ${response.status}`, | ||
| errorCode, | ||
| { | ||
| error: stringField(payload, "error") || void 0, | ||
| status: response.status | ||
| } | ||
| ); | ||
| } | ||
| const accessToken = stringField(payload, "access_token"); | ||
| const refreshToken = stringField(payload, "refresh_token") || fallback?.refreshToken || ""; | ||
| const tokenType = stringField(payload, "token_type") || "Bearer"; | ||
| const expiresIn = Number(payload.expires_in); | ||
| if (!accessToken || !refreshToken || !Number.isFinite(expiresIn) || expiresIn <= 0) { | ||
| throw new OAuthClientError("OAuth token response is incomplete", errorCode); | ||
| } | ||
| const returnedScopes = stringField(payload, "scope").split(/\s+/).filter(Boolean); | ||
| return { | ||
| accessToken, | ||
| expiresAt: Date.now() + expiresIn * 1e3, | ||
| refreshToken, | ||
| scope: returnedScopes.length > 0 ? returnedScopes : [...fallback?.scopes || []], | ||
| tokenType | ||
| }; | ||
| } | ||
| }; | ||
| var DEFAULT_SERVICE = "ai.mdedit.oauth"; | ||
| function normalizeProfiles(profiles) { | ||
| return [...new Set(profiles.map((profile) => profile.trim()).filter(Boolean))].sort(); | ||
| } | ||
| var OsCredentialStore = class { | ||
| createEntry; | ||
| loadProfileIndex; | ||
| saveProfileIndex; | ||
| service; | ||
| constructor({ | ||
| createEntry = (service2, account) => new Entry(service2, account), | ||
| loadProfileIndex, | ||
| saveProfileIndex, | ||
| service = DEFAULT_SERVICE | ||
| }) { | ||
| this.createEntry = createEntry; | ||
| this.loadProfileIndex = loadProfileIndex; | ||
| this.saveProfileIndex = saveProfileIndex; | ||
| this.service = service; | ||
| } | ||
| async delete(profile) { | ||
| this.createEntry(this.service, profile).deletePassword(); | ||
| const profiles = (await this.listProfiles()).filter((item) => item !== profile); | ||
| await this.saveProfileIndex(profiles); | ||
| } | ||
| async get(profile) { | ||
| const serialized = this.createEntry(this.service, profile).getPassword(); | ||
| if (!serialized) return null; | ||
| const parsed = JSON.parse(serialized); | ||
| if (!parsed || typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string" || typeof parsed.expiresAt !== "number" || !Array.isArray(parsed.scope)) { | ||
| throw new Error(`Stored OAuth profile "${profile}" is invalid`); | ||
| } | ||
| return parsed; | ||
| } | ||
| async listProfiles() { | ||
| return normalizeProfiles(await this.loadProfileIndex()); | ||
| } | ||
| async set(profile, tokenSet) { | ||
| this.createEntry(this.service, profile).setPassword(JSON.stringify(tokenSet)); | ||
| await this.saveProfileIndex(normalizeProfiles([ | ||
| ...await this.loadProfileIndex(), | ||
| profile | ||
| ])); | ||
| } | ||
| }; | ||
| // src/oauthCredentialProvider.ts | ||
| var API_RESOURCE = "https://apiv2.mdedit.ai/api"; | ||
@@ -1111,2 +1409,3 @@ function configPath(environment) { | ||
| ArticleSessionPool, | ||
| MDEDIT_MCP_OAUTH_RESOURCE, | ||
| MDEDIT_MCP_TOOL_SCOPES, | ||
@@ -1113,0 +1412,0 @@ createDurableMcpArticle, |
+5
-4
| { | ||
| "name": "@mdedit/mcp-server", | ||
| "version": "0.2.1", | ||
| "version": "0.2.2", | ||
| "private": false, | ||
@@ -49,6 +49,6 @@ "type": "module", | ||
| "dependencies": { | ||
| "@mdedit/agent-client": "1.0.0", | ||
| "@mdedit/node-auth": "0.1.0", | ||
| "@mdedit/sdk": "0.3.1", | ||
| "@mdedit/agent-client": "1.0.1", | ||
| "@mdedit/sdk": "0.3.2", | ||
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| "@napi-rs/keyring": "^1.3.0", | ||
| "zod": "^4.4.3" | ||
@@ -58,2 +58,3 @@ }, | ||
| "ajv": "^6.12.6", | ||
| "@mdedit/node-auth": "0.1.0", | ||
| "@types/node": "^20.10.5", | ||
@@ -60,0 +61,0 @@ "tsup": "^8.0.2", |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
167911
29.02%1585
24.51%0
-100%6
20%14
75%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
Updated
Updated