Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,19 @@ cannot independently prove who typed it. The MCP host must present the
destructive tool action, and the agent must pass only the user's actual words
and time. Agents must never fabricate either value.

Configure a public OAuth client ID in the extension's **Lumin OAuth Client ID**
This is currently a maintainer-configured integration, not a fully qualified
one-click signup experience. An ordinary user should not need to create a
developer app. If signing is not configured, ask the installation's maintainer;
creating a personal Lumin account alone does not enable the integration.

Once configured, ask PDF Tools to connect Lumin. Sign in and approve access in
the browser, never by sharing passwords or callback URLs in chat. If you need
an account, create it on Lumin's website. If signup does not return to the
connection before it expires, start the connection again. Connecting does not
upload a PDF, send an invitation, or approve a signature. After reconnecting,
check an existing request rather than sending it again.

For maintainers: configure a public OAuth client ID in the extension's **Lumin OAuth Client ID**
setting. Other stdio hosts may set `LUMIN_OAUTH_CLIENT_ID`; Agent Plugin users
may set `luminOAuthClientId` in the plugin's private `config.json`. Register the
exact redirect URI `http://127.0.0.1/callback`. The OAuth access token stays only
Expand All @@ -131,6 +143,12 @@ PKCE workflow. The durable signing-operation store currently supports macOS and
Linux. The public Lumin workflow fails closed on Windows until a reviewed
ACL-aware state adapter exists.

Lumin also provides its own hosted MCP and an API-key-based local extension.
These are separate connections, not automatically installed, authenticated, or
invoked by PDF Tools. See [Lumin onboarding and integration boundaries](docs/LUMIN_ONBOARDING.md)
for the capability comparison, first-time-user limitations, and attribution
proposal. No signup tracking or analytics reporting is enabled by this work.

### Page Organization Tools

- Merge multiple PDFs into one document
Expand Down
99 changes: 99 additions & 0 deletions docs/LUMIN_ONBOARDING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Lumin connection and first-time use

## Current experience

PDF Tools' direct Lumin integration currently requires a maintainer-configured
public OAuth application ID. It is not a consumer-ready shared application
configuration. No sandbox app ID is supplied as a default. Creating a personal
Lumin account does not fix missing installation configuration.

Once configured, `start_lumin_authorization` opens Lumin's authorization page
and returns instructions; `finish_lumin_authorization` completes the connection.
Account creation, passwords, and consent belong in Lumin's browser experience,
not MCP arguments or chat. A new user may need to create an account on Lumin's
website and restart connection if signup does not preserve the authorization
journey. Seamless signup continuation has not been qualified.

The local callback expires after five minutes. An expired or unsuccessful
connection can be restarted without uploading a PDF or sending an invitation.
Tokens remain in memory and a restart or token expiry requires reconnection.
Reconnect and poll an existing signing request; do not send a replacement.
Connecting an account never authorizes the separate document-send action.

## Lumin's own MCP: complement, not an automatic replacement

Research checked the official repository at
[`7c5ece34270a62752fc3298040b5414a95288999`](https://github.com/luminpdf/lumin-mcp-server/tree/7c5ece34270a62752fc3298040b5414a95288999)
and current developer documentation on 2026-09-05. Source and documentation
inspection is not a live authenticated compatibility test.

| Offering | What the inspected evidence establishes | Boundary |
| --- | --- | --- |
| Lumin local extension | Its manifest requires an API key; `stdio.js` passes that key to its tool server. Seven tools cover user/workspace information, upload, Markdown conversion, and send/status/cancel. | It does not remove account or credential setup. |
| Lumin hosted MCP | Official connection docs advertise `https://mcp.luminpdf.com/mcp`. The current tool reference lists 14 tools, including templates and agreement generation/download. | Hosted implementation, authentication persistence, and signup continuation were not executed or proven from the older local repository. |
| PDF Tools direct integration | Local PDF preparation, exact recipient preview and confirmation, direct upload, durable request outcome, status polling, and validated local artifact saving. | Separately configured account connection. No automatic federation or token sharing with Lumin's MCP. |

The inspected repository and published MCP tool list contain no account-signup
or attribution-reporting tool. That is not proof that Lumin lacks internal
signup or reporting services. The repository contains an OAuth admin helper
for creating OAuth clients, not user accounts; it requires server-side admin
configuration and must not be copied into a desktop client.

The hosted tool reference describes upload and signing from file URLs. A
local PDF path is not a remotely accessible URL. Do not expose a local file
server, upload to an intermediary, or publish a document merely to bridge the
two MCP servers. A future handoff needs an explicit private transfer contract
and the user's document-send consent. Neither connection's tokens, session
IDs, nor approval receipts are transferable to the other by assumption.

Prefer evaluating Lumin's hosted MCP as a companion for cloud-native templates,
workspace browsing, and cancellation before duplicating those tools locally.
The current integration does not install or call it. Two co-installed servers
must not both send the same request when one has already submitted it or has
an uncertain outcome.

## Attribution proposal, not enabled telemetry

The preferred first step is provider-side reporting against a dedicated,
ODA-owned production OAuth application identity. Confirm with Lumin that an
app registered in the owner's workspace can serve unrelated users/workspaces,
which workspace bears quotas/charges, and whether reporting can distinguish
PDF Tools traffic. Do not ship the test app as a production default.

Useful proposed measures are attributable new accounts, connected accounts,
unique signature requests sent, and completed/canceled/failed requests. A
successful authorization is not necessarily a new account. Polls are not new
requests. Provider outcomes must be deduplicated by request and state, rather
than counting every tool call or status poll as engagement.

The public docs inspected do not establish a signup referral mechanism or
per-app analytics dashboard. Lumin must confirm attribution across signup and
authorization. Do not invent referral parameters or repurpose OAuth `state`,
which is reserved here for connection security.

Local preparation, abandoned local attempts, and local errors are not covered
by provider-side reporting. Any future client analytics requires a separately
reviewed purpose, minimized schema, user-facing disclosure/control, retention,
and recipient. Do not report PDF content, prompts, paths, filenames, signer
names/emails, signatures, tokens, callback URLs, or signed download URLs as
analytics. No analytics collector, event emission, tracking identifier, signup
API, or additional network dependency is enabled by this change.

App webhooks are documented as private/server-app only and are not a reporting
shortcut for the current public PKCE desktop client. Server-side analytics
and private webhooks would be a separate architecture, not a reason to put
server secrets into the extension.

## Sources and qualification

- [Official repository manifest](https://github.com/luminpdf/lumin-mcp-server/blob/7c5ece34270a62752fc3298040b5414a95288999/manifest.json)
- [Official local entry point](https://github.com/luminpdf/lumin-mcp-server/blob/7c5ece34270a62752fc3298040b5414a95288999/stdio.js)
- [Official hosted connection](https://developers.luminpdf.com/tabs/mcp/connect)
- [Official current MCP tools](https://developers.luminpdf.com/tabs/mcp/supportedTool)
- [OAuth and public-client constraints](https://developers.luminpdf.com/tabs/guides/authentication/oauth2)
- [App-webhook restrictions](https://developers.luminpdf.com/tabs/guides/webhooks/app-webhooks)

The published redirect-URI prose still conflicts with the provider-confirmed,
tested loopback behavior. This research does not change the existing exact
`http://127.0.0.1/callback` registration or ephemeral-port implementation.
Do not infer free API allowance from Lumin's consumer web-plan pricing.
13 changes: 2 additions & 11 deletions pdf-toolkit-mcp-share/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import {
LUMIN_SIGNING_TOOL_DEFINITIONS,
LUMIN_SIGNING_TOOL_NAMES,
createLuminSigningToolHandler,
formatLuminSigningToolText,
} from "./lumin-signing-tools.js";

export const READ_CONTENT_ROUTING_GUIDANCE =
Expand Down Expand Up @@ -5033,17 +5034,7 @@ async function handleToolCall(request) {
try {
if (LUMIN_SIGNING_TOOL_NAME_SET.has(name)) {
const structuredContent = await handleLuminSigningTool(name, args ?? {});
const summary = name === "start_lumin_authorization"
? "Opened Lumin authorization in the browser."
: name === "finish_lumin_authorization"
? "Connected the Lumin account for this PDF Tools session."
: name === "prepare_lumin_request"
? "Prepared the Lumin signing request locally. Nothing was sent."
: name === "send_lumin_request"
? `Created Lumin signing request ${structuredContent.signature_request_id}.`
: name === "check_lumin_status"
? `Lumin signing status: ${structuredContent.provider_status}.`
: `Downloaded the Lumin ${structuredContent.file_type} PDF to ${structuredContent.pdf_path}.`;
const summary = formatLuminSigningToolText(name, structuredContent);
return {
content: [{ type: "text", text: summary }],
structuredContent,
Expand Down
34 changes: 28 additions & 6 deletions pdf-toolkit-mcp-share/server/lumin-signing-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const LUMIN_SIGNING_TOOL_DEFINITIONS = Object.freeze([
{
name: "start_lumin_authorization",
description:
"Start a secure browser login to Lumin using OAuth PKCE. This opens Lumin in the user's browser and returns an opaque local authorization session ID. It does not send a PDF or create a signing request. Call finish_lumin_authorization after the user finishes in the browser.",
"Connect a Lumin account through secure browser login using OAuth PKCE. Follow the returned next_step instructions. Account creation and passwords belong on Lumin's website, never in tool arguments or chat. If this installation is not configured, ask its maintainer to enable the integration rather than asking the user to create a developer app. This does not send a PDF or create a signing request. Call finish_lumin_authorization after the user finishes in the browser.",
inputSchema: {
type: "object",
properties: {},
Expand Down Expand Up @@ -213,6 +213,25 @@ export const LUMIN_SIGNING_TOOL_DEFINITIONS = Object.freeze([
},
]);

// Keep connection guidance in text as well as structuredContent for clients
// that primarily expose the text content of tool results to users/models.
export function formatLuminSigningToolText(name, result) {
const summary = name === "start_lumin_authorization"
? "Opened Lumin authorization in the browser."
: name === "finish_lumin_authorization"
? "Connected the Lumin account for this PDF Tools session."
: name === "prepare_lumin_request"
? "Prepared the Lumin signing request locally. Nothing was sent."
: name === "send_lumin_request"
? `Created Lumin signing request ${result.signature_request_id}.`
: name === "check_lumin_status"
? `Lumin signing status: ${result.provider_status}.`
: `Downloaded the Lumin ${result.file_type} PDF to ${result.pdf_path}.`;
return (name === "start_lumin_authorization" || name === "finish_lumin_authorization")
? `${summary} ${result.next_step}`
: summary;
}

function toolError(code, message) {
const error = new Error(`${code}: ${message}`);
error.code = code;
Expand Down Expand Up @@ -429,7 +448,7 @@ export function createLuminSigningToolHandler({
if (typeof clientId !== "string" || !clientId || clientId.includes("${")) {
throw toolError(
"LUMIN_OAUTH_NOT_CONFIGURED",
"Lumin signing is not configured. Add a public Lumin OAuth client ID to PDF Tools settings and restart PDF Tools.",
"Lumin signing is not enabled in this PDF Tools installation. Ask its maintainer to configure the public Lumin OAuth app. Creating a personal Lumin account will not fix this installation setting. Do not share a password, API key, or client secret in chat. Local PDF tools remain available.",
);
}
if (typeof stateRoot !== "string" || !path.isAbsolute(stateRoot)) {
Expand All @@ -442,7 +461,7 @@ export function createLuminSigningToolHandler({
const connection = connections.get(id);
if (!connection || connection.expiresAtMs <= currentMs() + 30_000) {
connections.delete(id);
throw toolError("LUMIN_AUTHORIZATION_REQUIRED", "Connect the Lumin account again before continuing.");
throw toolError("LUMIN_AUTHORIZATION_REQUIRED", "Connect the Lumin account again with start_lumin_authorization, then finish_lumin_authorization. This does not resend any signing request. Use check_lumin_status for an existing request, not send_lumin_request.");
}
return connection;
}
Expand Down Expand Up @@ -482,7 +501,7 @@ export function createLuminSigningToolHandler({
await openExternal(session.authorizationUrl);
} catch {
await session.close();
throw toolError("LUMIN_BROWSER_OPEN_FAILED", "PDF Tools could not open the Lumin authorization page.");
throw toolError("LUMIN_BROWSER_OPEN_FAILED", "PDF Tools could not open Lumin in your browser. Check your default browser, then try start_lumin_authorization again. No PDF or signing request was sent.");
}
pendingAuthorizations.set(id, {
session,
Expand All @@ -495,6 +514,7 @@ export function createLuminSigningToolHandler({
callback_expires_at: new Date(startedAtMs + AUTHORIZATION_TIMEOUT_MS).toISOString(),
browser_opened: true,
pdf_sent: false,
next_step: "Sign in on the Lumin page opened in your browser and review its access request. If you do not have an account, create one on Lumin's website, then return here. If signup does not return to this connection before it expires, run start_lumin_authorization again. Once you approve access in the browser, call finish_lumin_authorization with this session ID. Never paste passwords or browser callback URLs into chat.",
};
}

Expand All @@ -505,7 +525,8 @@ export function createLuminSigningToolHandler({
const pending = pendingAuthorizations.get(id);
if (!pending || pending.expiresAtMs <= currentMs()) {
pendingAuthorizations.delete(id);
throw toolError("LUMIN_AUTHORIZATION_SESSION_INVALID", "The Lumin authorization session is missing or expired.");
if (pending) await pending.session.close().catch(() => {});
throw toolError("LUMIN_AUTHORIZATION_SESSION_INVALID", "This Lumin connection attempt is missing or expired. Run start_lumin_authorization again, finish in the browser, then call finish_lumin_authorization with the new session ID. No signing request was sent.");
}
if (pending.completionInProgress) {
throw toolError("LUMIN_AUTHORIZATION_PENDING", "Lumin authorization is still waiting for the browser callback.");
Expand Down Expand Up @@ -542,7 +563,7 @@ export function createLuminSigningToolHandler({
throw sanitizedDependencyError(
error,
"LUMIN_OAUTH_TOKEN_REQUEST_FAILED",
"Lumin authorization could not be completed safely.",
"Lumin authorization was not completed. You can leave it disconnected or run start_lumin_authorization again when ready. No PDF or signing request was sent. Do not paste passwords or browser callback URLs into chat.",
);
} finally {
if (!pendingAuthorizations.has(id)) await pending.session.close().catch(() => {});
Expand All @@ -565,6 +586,7 @@ export function createLuminSigningToolHandler({
access_token_persisted: false,
refresh_token_persisted: false,
pdf_sent: false,
next_step: "Lumin is connected, but no PDF or signing request has been sent. For a new request, prepare the PDF with prepare_signing_packet, preview it with prepare_lumin_request, and obtain the user's exact sending confirmation before send_lumin_request. For an existing request, use check_lumin_status or download_lumin_artifact without sending again.",
};
}

Expand Down
2 changes: 2 additions & 0 deletions pdf-toolkit-mcp-share/server/output-schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -1803,6 +1803,7 @@ export const TOOL_SUCCESS_OUTPUT_SCHEMAS = Object.freeze({
}),
start_lumin_authorization: object({
status: { const: "awaiting_user_authorization" },
next_step: string,
provider: { const: "lumin_sign" },
authorization_session_id: string,
callback_expires_at: string,
Expand All @@ -1811,6 +1812,7 @@ export const TOOL_SUCCESS_OUTPUT_SCHEMAS = Object.freeze({
}),
finish_lumin_authorization: object({
status: { const: "connected" },
next_step: string,
provider: { const: "lumin_sign" },
authorization_session_id: string,
token_expires_at: string,
Expand Down
13 changes: 2 additions & 11 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import {
LUMIN_SIGNING_TOOL_DEFINITIONS,
LUMIN_SIGNING_TOOL_NAMES,
createLuminSigningToolHandler,
formatLuminSigningToolText,
} from "./lumin-signing-tools.js";

export const READ_CONTENT_ROUTING_GUIDANCE =
Expand Down Expand Up @@ -5033,17 +5034,7 @@ async function handleToolCall(request) {
try {
if (LUMIN_SIGNING_TOOL_NAME_SET.has(name)) {
const structuredContent = await handleLuminSigningTool(name, args ?? {});
const summary = name === "start_lumin_authorization"
? "Opened Lumin authorization in the browser."
: name === "finish_lumin_authorization"
? "Connected the Lumin account for this PDF Tools session."
: name === "prepare_lumin_request"
? "Prepared the Lumin signing request locally. Nothing was sent."
: name === "send_lumin_request"
? `Created Lumin signing request ${structuredContent.signature_request_id}.`
: name === "check_lumin_status"
? `Lumin signing status: ${structuredContent.provider_status}.`
: `Downloaded the Lumin ${structuredContent.file_type} PDF to ${structuredContent.pdf_path}.`;
const summary = formatLuminSigningToolText(name, structuredContent);
return {
content: [{ type: "text", text: summary }],
structuredContent,
Expand Down
Loading