urlpivot-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 URLPivot contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # URLPivot MCP
2
+
3
+ Keep the same public link and QR while your campaign destination changes. Connect your desktop agent to your URLPivot workspace to manage links, generate QR codes, prepare private Pages and review campaign traffic.
4
+
5
+ The hosted endpoint is available at `https://urlpivot.app/api/agent/mcp`. Native HTTP clients can connect directly; this package provides a stdio bridge for desktop clients.
6
+
7
+ ## Choose your connection
8
+
9
+ - Native HTTP clients: use `https://urlpivot.app/api/agent/mcp` and `Authorization: Bearer <workspace token>`. No installation required.
10
+ - Local stdio clients, including Claude Desktop configurations: this package bridges to the same hosted service using the official MCP SDK. It does not replace the service or host your data locally.
11
+ - Browser connectors that require OAuth: not supported by this Bearer-token bridge. This is not a Claude.ai or ChatGPT OAuth connector.
12
+
13
+ ## Start in five minutes
14
+
15
+ 1. Sign in at [URLPivot](https://urlpivot.app/login) and open the desired workspace.
16
+ 2. Open **AI agents → Agent access**. Create a workspace token with only the permissions you need. For your first connection, choose **Read only**.
17
+ 3. Store the token in the client's private environment as `URLPIVOT_TOKEN`. Never paste it into a conversation, URL, QR code or command argument.
18
+ 4. Configure an MCP stdio client (Node.js 22 or later):
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "urlpivot": {
24
+ "command": "npx",
25
+ "args": ["-y", "urlpivot-mcp@0.1.0"],
26
+ "env": { "URLPIVOT_TOKEN": "upv_YOUR_TOKEN" }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ Replace the placeholder only in your private client configuration. Restart the client, then read `urlpivot://capabilities` and run `tools/list`. The exposed tools depend on your token, workspace plan and backend; the public catalog is not an authorization grant.
33
+
34
+ To run from source, use `node /absolute/path/to/packages/mcp/bin/urlpivot-mcp.mjs` as your client's command after `npm ci --ignore-scripts` in this package directory. No token is needed for `--help` or `--version`.
35
+
36
+ ## Useful first tasks
37
+
38
+ - **Workspace checkup:** “Review my links, active campaigns and configured expiry. Prioritize what I should fix. Make no changes.”
39
+ - **Campaign setup:** “Prepare a Facebook campaign link. Suggest UTMs, show the final destination before saving, then return the short link and QR if my token permits it.”
40
+ - **Traffic review:** “Compare the available completed days for this campaign. Separate human clicks and bots, explain retention and suggest one next action.”
41
+ - **Private Page draft:** “Prepare a private Page from my existing links and approved brief. Return the editor URL for review. Do not publish.”
42
+
43
+ Native `prompts/list` / `prompts/get` become available when the connected server advertises prompts. Until then, use the text requests above with existing tools. This package forwards only advertised features and works with servers that expose tools/resources without prompts.
44
+
45
+ ## Permissions and limitations
46
+
47
+ The hosted catalog has 27 tools for workspace links, folders, QR, Pages, images, analytics, audit, domains and billing-access reads. Your token may expose fewer. New tools do not silently expand existing tokens. Page edits require the saved revision; publishing requires separate publication permissions. Domain tools return DNS/certificate instructions and request reconciliation; they do not edit DNS.
48
+
49
+ No billing mutations, account administration, workspace switching, bulk deletion, arbitrary browser access or native AI Page generation are provided by this package. Analytics counts retained classified events, not unique visitors or sales. A Free workspace can begin using scoped tools; paid product limits still apply. Installing the bridge does not upgrade a plan.
50
+
51
+ ## Troubleshooting
52
+
53
+ - Cannot connect / 401: check the endpoint, token validity and revocation.
54
+ - Permission failure / 403: check the token scopes and workspace plan; never try another workspace to bypass a denial.
55
+ - Stale Page revision: read the Page again before editing.
56
+ - Uncertain or timed-out write: read back before a manual retry. This bridge does not automatically retry writes.
57
+ - Native HTTP works but desktop does not: check Node.js 22+, private environment and the client's stdio config. Windows clients may require the absolute path to `npx.cmd`.
58
+ - Prompts missing: use `prompts/list` only when initialization advertises prompts. Older server versions have tools/resources without prompts.
59
+
60
+ `URLPIVOT_MCP_URL` optionally selects a trusted endpoint. HTTPS is required except loopback HTTP for development. URL credentials, query strings and fragments are rejected; redirects are rejected. Tokens are sent only as Authorization headers. No local token storage, telemetry or analytics collection is added by the bridge.
61
+
62
+ ## Contribute and get help
63
+
64
+ [Connection guide and tool catalog](https://urlpivot.app/agents) · [Machine-readable guide](https://urlpivot.app/llms.txt) · [Support](https://urlpivot.app/support)
65
+
66
+ For a bug report, include the package version, Node/client version, operation, sanitized error code and steps to reproduce. Omit tokens and private visitor data. Public repository/contribution links will be added after their actual publication; no community membership or certification is implied.
67
+
68
+ License: MIT for this bridge. URLPivot hosted service terms and plan limits remain separate.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createBridge, readBridgeConfig, VERSION } from '../src/bridge.mjs';
4
+
5
+ if (process.argv.includes('--help')) {
6
+ console.log('URLPivot MCP bridge\n\nSet URLPIVOT_TOKEN in your client private environment.\nURLPIVOT_MCP_URL is optional (default https://urlpivot.app/api/agent/mcp).\nRun without arguments for MCP stdio. Use --version for package version.');
7
+ } else if (process.argv.includes('--version')) {
8
+ console.log(VERSION);
9
+ } else if (process.argv.length > 2) {
10
+ console.error('No command arguments accepted. Store the token in URLPIVOT_TOKEN, never on the command line.');
11
+ process.exitCode = 1;
12
+ } else {
13
+ let bridge;
14
+ try {
15
+ bridge = await createBridge(readBridgeConfig());
16
+ const stop = async () => { await bridge.close(); process.exitCode = 0; };
17
+ process.once('SIGINT', stop);
18
+ process.once('SIGTERM', stop);
19
+ process.stdin.once('end', stop);
20
+ await bridge.server.connect(new StdioServerTransport());
21
+ } catch (error) {
22
+ await bridge?.close();
23
+ // Only our controlled startup messages are allowed here. Never echo a token or upstream body.
24
+ console.error(error instanceof Error && /^(Set URLPIVOT_TOKEN|URLPIVOT_MCP_URL|Use HTTPS|Cannot connect to URLPivot)/.test(error.message)
25
+ ? error.message : 'URLPivot MCP could not start. Check the private client configuration.');
26
+ process.exitCode = 1;
27
+ }
28
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "urlpivot-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "app.urlpivot/mcp",
5
+ "description": "Connect MCP desktop agents to URLPivot links, QR codes, Pages and campaign analytics using a scoped workspace token.",
6
+ "type": "module",
7
+ "bin": { "urlpivot-mcp": "bin/urlpivot-mcp.mjs" },
8
+ "exports": "./src/bridge.mjs",
9
+ "files": ["bin", "src", "README.md", "LICENSE"],
10
+ "engines": { "node": ">=22" },
11
+ "scripts": { "test": "node --test test/*.test.mjs", "start": "node bin/urlpivot-mcp.mjs" },
12
+ "keywords": ["mcp", "model-context-protocol", "urlpivot", "link-management", "qr-code", "campaign-analytics", "claude-desktop", "ai-agents"],
13
+ "homepage": "https://urlpivot.app/agents",
14
+ "bugs": { "url": "https://urlpivot.app/support" },
15
+ "license": "MIT",
16
+ "publishConfig": { "access": "public" },
17
+ "dependencies": { "@modelcontextprotocol/sdk": "1.32.0", "zod": "4.6.5" }
18
+ }
package/src/bridge.mjs ADDED
@@ -0,0 +1,81 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport, StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
4
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
+ import {
6
+ CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema,
7
+ ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, McpError,
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+
10
+ export const VERSION = '0.1.0';
11
+ export const DEFAULT_ENDPOINT = 'https://urlpivot.app/api/agent/mcp';
12
+
13
+ export function readBridgeConfig(env = process.env) {
14
+ const token = env.URLPIVOT_TOKEN;
15
+ if (!token || !/^upv_[A-Za-z0-9_-]{16,512}$/.test(token)) throw new Error('Set URLPIVOT_TOKEN to a workspace agent token. Do not pass it in command arguments.');
16
+ let endpoint;
17
+ try { endpoint = new URL(env.URLPIVOT_MCP_URL || DEFAULT_ENDPOINT); }
18
+ catch { throw new Error('URLPIVOT_MCP_URL must be a valid HTTPS endpoint.'); }
19
+ const loopback = ['127.0.0.1', 'localhost', '[::1]'].includes(endpoint.hostname);
20
+ if ((endpoint.protocol !== 'https:' && !(endpoint.protocol === 'http:' && loopback)) || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
21
+ throw new Error('Use HTTPS (HTTP only on loopback), without credentials, query strings or fragments.');
22
+ }
23
+ return { endpoint, token };
24
+ }
25
+
26
+ /** No account switching, local persistence, telemetry or retry of tool mutations. */
27
+ export async function createBridge(config, { fetch: fetchOverride } = {}) {
28
+ const remote = new Client({ name: 'urlpivot-mcp', version: VERSION }, { capabilities: {} });
29
+ const guardedFetch = async (url, init) => {
30
+ const response = await (fetchOverride || fetch)(url, { ...init, redirect: 'manual' });
31
+ if (response.status >= 300 && response.status < 400) {
32
+ await response.body?.cancel();
33
+ throw new Error('Endpoint redirects are not permitted.');
34
+ }
35
+ return response;
36
+ };
37
+ const transport = new StreamableHTTPClientTransport(config.endpoint, {
38
+ requestInit: { headers: { Authorization: `Bearer ${config.token}` }, redirect: 'error' },
39
+ fetch: guardedFetch,
40
+ });
41
+ // Never print raw transport failures, response bodies, headers or credentials.
42
+ remote.onerror = () => {};
43
+ try { await remote.connect(transport); }
44
+ catch { await remote.close().catch(() => {}); throw new Error('Cannot connect to URLPivot. Check the endpoint and token validity; no tool has been called.'); }
45
+ const capabilities = remote.getServerCapabilities() || {};
46
+ const server = new Server({ name: 'urlpivot-mcp', version: VERSION }, {
47
+ capabilities: {
48
+ ...(capabilities.tools ? { tools: {} } : {}),
49
+ ...(capabilities.resources ? { resources: {} } : {}),
50
+ ...(capabilities.prompts ? { prompts: {} } : {}),
51
+ },
52
+ instructions: remote.getInstructions(),
53
+ });
54
+ const forward = (run) => async (request, extra) => {
55
+ try { return await run(request.params, { signal: extra.signal, timeout: 60_000 }); }
56
+ catch (error) {
57
+ const code = error instanceof McpError ? error.code
58
+ : error instanceof UnauthorizedError ? -32001
59
+ : error instanceof StreamableHTTPError && error.code === 401 ? -32001
60
+ : error instanceof StreamableHTTPError && error.code === 403 ? -32003
61
+ : -32603;
62
+ throw new McpError(code, 'URLPivot request failed. Check token permissions, input and saved revision. A failed or timed-out write is not proof that it was not saved; read back before a manual retry.');
63
+ }
64
+ };
65
+ if (capabilities.tools) {
66
+ server.setRequestHandler(ListToolsRequestSchema, forward((params, options) => remote.listTools(params, options)));
67
+ server.setRequestHandler(CallToolRequestSchema, forward((params, options) => remote.callTool(params, undefined, options)));
68
+ }
69
+ if (capabilities.resources) {
70
+ server.setRequestHandler(ListResourcesRequestSchema, forward((params, options) => remote.listResources(params, options)));
71
+ server.setRequestHandler(ReadResourceRequestSchema, forward((params, options) => remote.readResource(params, options)));
72
+ }
73
+ if (capabilities.prompts) {
74
+ server.setRequestHandler(ListPromptsRequestSchema, forward((params, options) => remote.listPrompts(params, options)));
75
+ server.setRequestHandler(GetPromptRequestSchema, forward((params, options) => remote.getPrompt(params, options)));
76
+ }
77
+ return {
78
+ server,
79
+ async close() { await Promise.allSettled([server.close(), remote.close()]); },
80
+ };
81
+ }