🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@mdedit/mcp-server

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mdedit/mcp-server - npm Package Compare versions

Comparing version
0.2.1
to
0.2.2
+19
-3
dist/index.d.ts
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 };

@@ -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