@wootsup/mcp 0.1.0-rc.1
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/CHANGELOG.md +91 -0
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/SECURITY.md +163 -0
- package/dist/auth/keychain.d.ts +47 -0
- package/dist/auth/keychain.js +262 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/oauth-provider.d.ts +68 -0
- package/dist/auth/oauth-provider.js +232 -0
- package/dist/auth/oauth-provider.js.map +1 -0
- package/dist/auth/profiles.d.ts +52 -0
- package/dist/auth/profiles.js +200 -0
- package/dist/auth/profiles.js.map +1 -0
- package/dist/auth/token.d.ts +27 -0
- package/dist/auth/token.js +88 -0
- package/dist/auth/token.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/dist/install-skill.d.ts +23 -0
- package/dist/install-skill.js +73 -0
- package/dist/install-skill.js.map +1 -0
- package/dist/modules/apimapper/cache.d.ts +2 -0
- package/dist/modules/apimapper/cache.js +71 -0
- package/dist/modules/apimapper/cache.js.map +1 -0
- package/dist/modules/apimapper/client.d.ts +85 -0
- package/dist/modules/apimapper/client.js +523 -0
- package/dist/modules/apimapper/client.js.map +1 -0
- package/dist/modules/apimapper/connections.d.ts +2 -0
- package/dist/modules/apimapper/connections.js +406 -0
- package/dist/modules/apimapper/connections.js.map +1 -0
- package/dist/modules/apimapper/credential-sanitizer.d.ts +7 -0
- package/dist/modules/apimapper/credential-sanitizer.js +70 -0
- package/dist/modules/apimapper/credential-sanitizer.js.map +1 -0
- package/dist/modules/apimapper/credentials.d.ts +2 -0
- package/dist/modules/apimapper/credentials.js +258 -0
- package/dist/modules/apimapper/credentials.js.map +1 -0
- package/dist/modules/apimapper/diagnose.d.ts +18 -0
- package/dist/modules/apimapper/diagnose.js +305 -0
- package/dist/modules/apimapper/diagnose.js.map +1 -0
- package/dist/modules/apimapper/flows.d.ts +2 -0
- package/dist/modules/apimapper/flows.js +372 -0
- package/dist/modules/apimapper/flows.js.map +1 -0
- package/dist/modules/apimapper/get-skill.d.ts +4 -0
- package/dist/modules/apimapper/get-skill.js +88 -0
- package/dist/modules/apimapper/get-skill.js.map +1 -0
- package/dist/modules/apimapper/graph-builder.d.ts +47 -0
- package/dist/modules/apimapper/graph-builder.js +117 -0
- package/dist/modules/apimapper/graph-builder.js.map +1 -0
- package/dist/modules/apimapper/graph.d.ts +2 -0
- package/dist/modules/apimapper/graph.js +117 -0
- package/dist/modules/apimapper/graph.js.map +1 -0
- package/dist/modules/apimapper/index.d.ts +2 -0
- package/dist/modules/apimapper/index.js +43 -0
- package/dist/modules/apimapper/index.js.map +1 -0
- package/dist/modules/apimapper/inspect.d.ts +20 -0
- package/dist/modules/apimapper/inspect.js +86 -0
- package/dist/modules/apimapper/inspect.js.map +1 -0
- package/dist/modules/apimapper/library.d.ts +2 -0
- package/dist/modules/apimapper/library.js +237 -0
- package/dist/modules/apimapper/library.js.map +1 -0
- package/dist/modules/apimapper/license.d.ts +2 -0
- package/dist/modules/apimapper/license.js +142 -0
- package/dist/modules/apimapper/license.js.map +1 -0
- package/dist/modules/apimapper/local-sources.d.ts +2 -0
- package/dist/modules/apimapper/local-sources.js +123 -0
- package/dist/modules/apimapper/local-sources.js.map +1 -0
- package/dist/modules/apimapper/misc.d.ts +2 -0
- package/dist/modules/apimapper/misc.js +149 -0
- package/dist/modules/apimapper/misc.js.map +1 -0
- package/dist/modules/apimapper/node-schema.d.ts +217 -0
- package/dist/modules/apimapper/node-schema.js +218 -0
- package/dist/modules/apimapper/node-schema.js.map +1 -0
- package/dist/modules/apimapper/normalizers.d.ts +13 -0
- package/dist/modules/apimapper/normalizers.js +37 -0
- package/dist/modules/apimapper/normalizers.js.map +1 -0
- package/dist/modules/apimapper/onboarding.d.ts +51 -0
- package/dist/modules/apimapper/onboarding.js +201 -0
- package/dist/modules/apimapper/onboarding.js.map +1 -0
- package/dist/modules/apimapper/schema.d.ts +2 -0
- package/dist/modules/apimapper/schema.js +84 -0
- package/dist/modules/apimapper/schema.js.map +1 -0
- package/dist/modules/apimapper/settings.d.ts +2 -0
- package/dist/modules/apimapper/settings.js +157 -0
- package/dist/modules/apimapper/settings.js.map +1 -0
- package/dist/modules/apimapper/skill-resources.d.ts +4 -0
- package/dist/modules/apimapper/skill-resources.js +85 -0
- package/dist/modules/apimapper/skill-resources.js.map +1 -0
- package/dist/modules/apimapper/types.d.ts +111 -0
- package/dist/modules/apimapper/types.js +14 -0
- package/dist/modules/apimapper/types.js.map +1 -0
- package/dist/modules/apimapper/use-profile.d.ts +34 -0
- package/dist/modules/apimapper/use-profile.js +176 -0
- package/dist/modules/apimapper/use-profile.js.map +1 -0
- package/dist/modules/apimapper/workflows.d.ts +2 -0
- package/dist/modules/apimapper/workflows.js +301 -0
- package/dist/modules/apimapper/workflows.js.map +1 -0
- package/dist/platform/index.d.ts +71 -0
- package/dist/platform/index.js +377 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/server-http.d.ts +22 -0
- package/dist/server-http.js +159 -0
- package/dist/server-http.js.map +1 -0
- package/dist/setup/detect-clients.d.ts +39 -0
- package/dist/setup/detect-clients.js +152 -0
- package/dist/setup/detect-clients.js.map +1 -0
- package/dist/setup/probe-handshake.d.ts +26 -0
- package/dist/setup/probe-handshake.js +159 -0
- package/dist/setup/probe-handshake.js.map +1 -0
- package/dist/setup/write-config.d.ts +25 -0
- package/dist/setup/write-config.js +247 -0
- package/dist/setup/write-config.js.map +1 -0
- package/dist/setup-cli.d.ts +49 -0
- package/dist/setup-cli.js +292 -0
- package/dist/setup-cli.js.map +1 -0
- package/dist/skill-instructions.d.ts +10 -0
- package/dist/skill-instructions.js +68 -0
- package/dist/skill-instructions.js.map +1 -0
- package/dist/transports/http.d.ts +29 -0
- package/dist/transports/http.js +267 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/stdio.d.ts +9 -0
- package/dist/transports/stdio.js +19 -0
- package/dist/transports/stdio.js.map +1 -0
- package/docs/architecture.md +140 -0
- package/docs/customgraph-internal-migration.md +210 -0
- package/docs/security.md +126 -0
- package/docs/tools.md +230 -0
- package/manifest.json +76 -0
- package/package.json +61 -0
- package/skills/apimapper/SKILL.md +57 -0
- package/skills/apimapper/reference/joomla.md +85 -0
- package/skills/apimapper/reference/oauth.md +94 -0
- package/skills/apimapper/reference/troubleshooting.md +123 -0
- package/skills/apimapper/reference/yootheme.md +96 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/skill-instructions.ts — Reads the bundled apimapper skill's SKILL.md
|
|
2
|
+
// and returns the body (post-frontmatter) truncated to 500 chars, for use
|
|
3
|
+
// as the McpServer `instructions` field.
|
|
4
|
+
//
|
|
5
|
+
// Falls back to a one-line default if the file is missing or unreadable.
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
export const FALLBACK_INSTRUCTIONS = "Use this MCP to manage API Mapper flows, connections, and sources across WordPress + Joomla.";
|
|
10
|
+
const MAX_LENGTH = 500;
|
|
11
|
+
/**
|
|
12
|
+
* Loads MCP server instructions from skills/apimapper/SKILL.md.
|
|
13
|
+
*
|
|
14
|
+
* @param rootDir - Override the package root directory (used by tests).
|
|
15
|
+
* Defaults to the directory two levels up from this file
|
|
16
|
+
* (i.e. the package root), so that `skills/apimapper/SKILL.md`
|
|
17
|
+
* resolves correctly both in dev (src/) and in build (dist/).
|
|
18
|
+
*/
|
|
19
|
+
export function loadSkillInstructions(rootDir) {
|
|
20
|
+
const baseDir = rootDir ?? defaultRoot();
|
|
21
|
+
const skillPath = join(baseDir, "skills", "apimapper", "SKILL.md");
|
|
22
|
+
if (!existsSync(skillPath)) {
|
|
23
|
+
return FALLBACK_INSTRUCTIONS;
|
|
24
|
+
}
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = readFileSync(skillPath, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return FALLBACK_INSTRUCTIONS;
|
|
31
|
+
}
|
|
32
|
+
const body = stripFrontmatter(raw).trim();
|
|
33
|
+
if (body.length === 0) {
|
|
34
|
+
return FALLBACK_INSTRUCTIONS;
|
|
35
|
+
}
|
|
36
|
+
return body.slice(0, MAX_LENGTH);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Strips a YAML frontmatter block (`---\n...\n---\n`) from the start of the
|
|
40
|
+
* input. Returns the original string if no frontmatter is detected.
|
|
41
|
+
*
|
|
42
|
+
* Tiny inline parser — gray-matter would pull in dependencies we don't need.
|
|
43
|
+
*/
|
|
44
|
+
function stripFrontmatter(input) {
|
|
45
|
+
if (!input.startsWith("---")) {
|
|
46
|
+
return input;
|
|
47
|
+
}
|
|
48
|
+
// Find the closing fence. Must be at line start.
|
|
49
|
+
const closing = input.indexOf("\n---", 3);
|
|
50
|
+
if (closing < 0) {
|
|
51
|
+
return input;
|
|
52
|
+
}
|
|
53
|
+
// Advance past the closing fence and the newline that follows it.
|
|
54
|
+
const after = input.indexOf("\n", closing + 4);
|
|
55
|
+
if (after < 0) {
|
|
56
|
+
// Closing fence is the last line; return empty body.
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
return input.slice(after + 1);
|
|
60
|
+
}
|
|
61
|
+
function defaultRoot() {
|
|
62
|
+
// This file lives at <pkg-root>/src/skill-instructions.ts (or
|
|
63
|
+
// <pkg-root>/dist/skill-instructions.js after build). In both cases,
|
|
64
|
+
// the package root is one directory up.
|
|
65
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
66
|
+
return resolve(here, "..");
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=skill-instructions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-instructions.js","sourceRoot":"","sources":["../src/skill-instructions.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,0EAA0E;AAC1E,yCAAyC;AACzC,EAAE;AACF,yEAAyE;AAEzE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAChC,8FAA8F,CAAC;AAEjG,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,MAAM,OAAO,GAAG,OAAO,IAAI,WAAW,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;IAEnE,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IAED,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IAED,MAAM,IAAI,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,KAAa;IACrC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,iDAAiD;IACjD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC1C,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,kEAAkE;IAClE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;IAC/C,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,qDAAqD;QACrD,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,WAAW;IAClB,8DAA8D;IAC9D,qEAAqE;IACrE,wCAAwC;IACxC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,OAAO,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import type { OAuthProvider } from "../auth/oauth-provider.js";
|
|
3
|
+
export interface HttpAuthInfo {
|
|
4
|
+
client_id: string;
|
|
5
|
+
scope: string;
|
|
6
|
+
}
|
|
7
|
+
export type McpHandler = (body: unknown, authInfo: HttpAuthInfo | null) => Promise<unknown>;
|
|
8
|
+
export interface HttpTransportOptions {
|
|
9
|
+
/** TCP port to bind. Use 0 for an ephemeral port. */
|
|
10
|
+
port: number;
|
|
11
|
+
/** OAuth provider state — typically a singleton per process. */
|
|
12
|
+
oauth: OAuthProvider;
|
|
13
|
+
/** MCP routing callback. Receives parsed JSON body + auth info. */
|
|
14
|
+
mcpHandler: McpHandler;
|
|
15
|
+
/**
|
|
16
|
+
* Public base URL used in OAuth metadata. Default: derived from the
|
|
17
|
+
* bound address (`http://127.0.0.1:<port>`).
|
|
18
|
+
*/
|
|
19
|
+
publicBaseUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface HttpTransportServer {
|
|
22
|
+
/** http.Server instance. */
|
|
23
|
+
server: Server;
|
|
24
|
+
/** Resolved base URL (with port). */
|
|
25
|
+
url: string;
|
|
26
|
+
/** Stop accepting new connections + close existing keep-alives. */
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createHttpTransportServer(options: HttpTransportOptions): Promise<HttpTransportServer>;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/transports/http.ts — Phase 9.2.
|
|
2
|
+
//
|
|
3
|
+
// Native node:http server that exposes:
|
|
4
|
+
//
|
|
5
|
+
// GET /health — readiness probe
|
|
6
|
+
// GET /.well-known/oauth-authorization-server — RFC 8414 metadata
|
|
7
|
+
// POST /register — RFC 7591 Dynamic Client Registration
|
|
8
|
+
// GET /authorize — OAuth 2.0 Authorization endpoint
|
|
9
|
+
// POST /token — OAuth 2.0 Token endpoint
|
|
10
|
+
// POST /mcp — MCP JSON-RPC (Bearer-gated)
|
|
11
|
+
//
|
|
12
|
+
// The MCP routing is intentionally pluggable via the `mcpHandler` callback:
|
|
13
|
+
// this file is responsible ONLY for HTTP framing + OAuth auth. The boot
|
|
14
|
+
// entry `src/server-http.ts` glues this transport to a real MCP server by
|
|
15
|
+
// supplying a handler that uses the SDK's StreamableHTTPServerTransport.
|
|
16
|
+
//
|
|
17
|
+
// Why hand-rolled HTTP instead of Express? We already have @modelcontextprotocol/sdk
|
|
18
|
+
// + zod + clack as runtime deps; adding Express would balloon the DXT bundle
|
|
19
|
+
// for ~50 lines of routing logic we own outright.
|
|
20
|
+
import { createServer as createHttpServer } from "node:http";
|
|
21
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
22
|
+
function send(res, status, body, extraHeaders = {}) {
|
|
23
|
+
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
|
24
|
+
res.writeHead(status, {
|
|
25
|
+
"content-type": typeof body === "string" ? "text/plain; charset=utf-8" : "application/json",
|
|
26
|
+
"content-length": String(Buffer.byteLength(payload)),
|
|
27
|
+
...extraHeaders,
|
|
28
|
+
});
|
|
29
|
+
res.end(payload);
|
|
30
|
+
}
|
|
31
|
+
function sendError(res, status, error, description) {
|
|
32
|
+
const body = { error };
|
|
33
|
+
if (description)
|
|
34
|
+
body.error_description = description;
|
|
35
|
+
send(res, status, body);
|
|
36
|
+
}
|
|
37
|
+
async function readBody(req) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
req.on("data", (c) => chunks.push(c));
|
|
41
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
42
|
+
req.on("error", reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function parseJson(raw) {
|
|
46
|
+
if (!raw)
|
|
47
|
+
return {};
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getBearer(req) {
|
|
56
|
+
const hdr = req.headers["authorization"];
|
|
57
|
+
if (typeof hdr !== "string")
|
|
58
|
+
return null;
|
|
59
|
+
const m = /^Bearer\s+(.+)$/i.exec(hdr);
|
|
60
|
+
return m ? m[1] : null;
|
|
61
|
+
}
|
|
62
|
+
// ── Factory ────────────────────────────────────────────────────────────
|
|
63
|
+
export async function createHttpTransportServer(options) {
|
|
64
|
+
const { oauth, mcpHandler } = options;
|
|
65
|
+
// We compute the public base URL lazily — for port: 0 we need it after
|
|
66
|
+
// listen() resolves.
|
|
67
|
+
let publicBaseUrl = options.publicBaseUrl ?? "";
|
|
68
|
+
const server = createHttpServer(async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
await handle(req, res);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
// Never let the listener throw — surface as a 500 with a sanitized body.
|
|
74
|
+
const msg = err instanceof Error ? err.message : "unknown_error";
|
|
75
|
+
sendError(res, 500, "server_error", msg);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
async function handle(req, res) {
|
|
79
|
+
const url = new URL(req.url ?? "/", publicBaseUrl || "http://127.0.0.1");
|
|
80
|
+
const path = url.pathname;
|
|
81
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
82
|
+
// ── /health ──────────────────────────────────────────────────────
|
|
83
|
+
if (path === "/health" && method === "GET") {
|
|
84
|
+
send(res, 200, { status: "ok", transport: "http" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// ── /.well-known/oauth-authorization-server ──────────────────────
|
|
88
|
+
if (path === "/.well-known/oauth-authorization-server" &&
|
|
89
|
+
method === "GET") {
|
|
90
|
+
send(res, 200, {
|
|
91
|
+
issuer: publicBaseUrl,
|
|
92
|
+
authorization_endpoint: `${publicBaseUrl}/authorize`,
|
|
93
|
+
token_endpoint: `${publicBaseUrl}/token`,
|
|
94
|
+
registration_endpoint: `${publicBaseUrl}/register`,
|
|
95
|
+
response_types_supported: ["code"],
|
|
96
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
97
|
+
code_challenge_methods_supported: ["S256"],
|
|
98
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
99
|
+
scopes_supported: ["read", "write", "admin"],
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// ── POST /register (RFC 7591 DCR) ────────────────────────────────
|
|
104
|
+
if (path === "/register" && method === "POST") {
|
|
105
|
+
const raw = await readBody(req);
|
|
106
|
+
const body = parseJson(raw);
|
|
107
|
+
if (body === null || typeof body !== "object") {
|
|
108
|
+
sendError(res, 400, "invalid_client_metadata", "body must be JSON");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const b = body;
|
|
112
|
+
const redirectUris = Array.isArray(b.redirect_uris)
|
|
113
|
+
? b.redirect_uris.filter((u) => typeof u === "string")
|
|
114
|
+
: [];
|
|
115
|
+
const clientName = typeof b.client_name === "string" ? b.client_name : undefined;
|
|
116
|
+
try {
|
|
117
|
+
const reg = oauth.registerClient({
|
|
118
|
+
redirect_uris: redirectUris,
|
|
119
|
+
client_name: clientName,
|
|
120
|
+
});
|
|
121
|
+
send(res, 201, reg);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const msg = err instanceof Error ? err.message : "invalid_client_metadata";
|
|
125
|
+
sendError(res, 400, "invalid_client_metadata", msg);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// ── GET /authorize ───────────────────────────────────────────────
|
|
130
|
+
if (path === "/authorize" && method === "GET") {
|
|
131
|
+
const q = url.searchParams;
|
|
132
|
+
const clientId = q.get("client_id") ?? "";
|
|
133
|
+
const redirectUri = q.get("redirect_uri") ?? "";
|
|
134
|
+
const responseType = q.get("response_type") ?? "";
|
|
135
|
+
const codeChallenge = q.get("code_challenge") ?? "";
|
|
136
|
+
const codeChallengeMethod = q.get("code_challenge_method") ?? "";
|
|
137
|
+
const scope = q.get("scope") ?? "";
|
|
138
|
+
const state = q.get("state") ?? undefined;
|
|
139
|
+
if (responseType !== "code") {
|
|
140
|
+
sendError(res, 400, "unsupported_response_type");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const client = oauth.getClient(clientId);
|
|
144
|
+
if (!client) {
|
|
145
|
+
sendError(res, 400, "unknown_client");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!client.redirect_uris.includes(redirectUri)) {
|
|
149
|
+
sendError(res, 400, "invalid_redirect_uri");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (codeChallengeMethod !== "S256") {
|
|
153
|
+
sendError(res, 400, "invalid_request", "code_challenge_method must be S256");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let code;
|
|
157
|
+
try {
|
|
158
|
+
code = oauth.issueCode({
|
|
159
|
+
client_id: clientId,
|
|
160
|
+
redirect_uri: redirectUri,
|
|
161
|
+
code_challenge: codeChallenge,
|
|
162
|
+
code_challenge_method: "S256",
|
|
163
|
+
scope,
|
|
164
|
+
state,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const msg = err instanceof Error ? err.message : "invalid_request";
|
|
169
|
+
sendError(res, 400, "invalid_request", msg);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const cbUrl = new URL(redirectUri);
|
|
173
|
+
cbUrl.searchParams.set("code", code);
|
|
174
|
+
if (state !== undefined)
|
|
175
|
+
cbUrl.searchParams.set("state", state);
|
|
176
|
+
res.writeHead(302, { location: cbUrl.toString() });
|
|
177
|
+
res.end();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// ── POST /token ──────────────────────────────────────────────────
|
|
181
|
+
if (path === "/token" && method === "POST") {
|
|
182
|
+
const raw = await readBody(req);
|
|
183
|
+
const params = new URLSearchParams(raw);
|
|
184
|
+
const grantType = params.get("grant_type") ?? "";
|
|
185
|
+
try {
|
|
186
|
+
if (grantType === "authorization_code") {
|
|
187
|
+
const tokens = oauth.exchangeCode({
|
|
188
|
+
client_id: params.get("client_id") ?? "",
|
|
189
|
+
code: params.get("code") ?? "",
|
|
190
|
+
redirect_uri: params.get("redirect_uri") ?? "",
|
|
191
|
+
code_verifier: params.get("code_verifier") ?? "",
|
|
192
|
+
});
|
|
193
|
+
send(res, 200, tokens);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (grantType === "refresh_token") {
|
|
197
|
+
const tokens = oauth.refreshToken({
|
|
198
|
+
client_id: params.get("client_id") ?? "",
|
|
199
|
+
refresh_token: params.get("refresh_token") ?? "",
|
|
200
|
+
});
|
|
201
|
+
send(res, 200, tokens);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
sendError(res, 400, "unsupported_grant_type");
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : "invalid_grant";
|
|
208
|
+
sendError(res, 400, "invalid_grant", msg);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// ── POST /mcp ────────────────────────────────────────────────────
|
|
213
|
+
if (path === "/mcp" && method === "POST") {
|
|
214
|
+
const token = getBearer(req);
|
|
215
|
+
if (!token) {
|
|
216
|
+
send(res, 401, { error: "unauthorized", error_description: "Bearer token required" }, {
|
|
217
|
+
"www-authenticate": `Bearer realm="${publicBaseUrl}", error="invalid_token"`,
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const info = oauth.verifyAccessToken(token);
|
|
222
|
+
if (!info) {
|
|
223
|
+
send(res, 401, {
|
|
224
|
+
error: "invalid_token",
|
|
225
|
+
error_description: "Bearer token unknown or expired",
|
|
226
|
+
}, {
|
|
227
|
+
"www-authenticate": `Bearer realm="${publicBaseUrl}", error="invalid_token"`,
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const raw = await readBody(req);
|
|
232
|
+
const body = parseJson(raw);
|
|
233
|
+
if (body === null) {
|
|
234
|
+
sendError(res, 400, "invalid_request", "body must be JSON");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const result = await mcpHandler(body, {
|
|
238
|
+
client_id: info.client_id,
|
|
239
|
+
scope: info.scope,
|
|
240
|
+
});
|
|
241
|
+
send(res, 200, result);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// ── 404 fallback ─────────────────────────────────────────────────
|
|
245
|
+
sendError(res, 404, "not_found", `no route for ${method} ${path}`);
|
|
246
|
+
}
|
|
247
|
+
// Bind + resolve base URL
|
|
248
|
+
await new Promise((resolve) => server.listen(options.port, resolve));
|
|
249
|
+
const addr = server.address();
|
|
250
|
+
if (!publicBaseUrl) {
|
|
251
|
+
publicBaseUrl = `http://127.0.0.1:${addr.port}`;
|
|
252
|
+
}
|
|
253
|
+
else if (publicBaseUrl === "http://127.0.0.1" ||
|
|
254
|
+
publicBaseUrl === "http://localhost") {
|
|
255
|
+
publicBaseUrl = `${publicBaseUrl}:${addr.port}`;
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
server,
|
|
259
|
+
url: publicBaseUrl,
|
|
260
|
+
close: () => new Promise((resolve, reject) => {
|
|
261
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
262
|
+
// Force-close keep-alive sockets so tests don't hang.
|
|
263
|
+
server.closeAllConnections?.();
|
|
264
|
+
}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Connect the given McpServer to a fresh `StdioServerTransport`.
|
|
4
|
+
*
|
|
5
|
+
* Resolves once the SDK has finished its handshake. Rejects with the
|
|
6
|
+
* underlying SDK error if `server.connect()` throws — callers should treat
|
|
7
|
+
* a rejection as a fatal startup error.
|
|
8
|
+
*/
|
|
9
|
+
export declare function connectStdio(server: McpServer): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/transports/stdio.ts — Phase 9.1.
|
|
2
|
+
//
|
|
3
|
+
// Thin factory that connects an McpServer to a StdioServerTransport so the
|
|
4
|
+
// process speaks JSON-RPC over stdin/stdout. Extracted from src/index.ts as
|
|
5
|
+
// part of the multi-transport split — Task 9.2 introduces an HTTP variant
|
|
6
|
+
// using the same `connect*` shape.
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
/**
|
|
9
|
+
* Connect the given McpServer to a fresh `StdioServerTransport`.
|
|
10
|
+
*
|
|
11
|
+
* Resolves once the SDK has finished its handshake. Rejects with the
|
|
12
|
+
* underlying SDK error if `server.connect()` throws — callers should treat
|
|
13
|
+
* a rejection as a fatal startup error.
|
|
14
|
+
*/
|
|
15
|
+
export async function connectStdio(server) {
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/transports/stdio.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,2EAA2E;AAC3E,4EAA4E;AAC5E,0EAA0E;AAC1E,mCAAmC;AAGnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAiB;IAClD,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
`@wootsup/mcp` is the MCP server side of the API Mapper product.
|
|
4
|
+
This document is intentionally short — for the full design, see the
|
|
5
|
+
internal plan-doc `~/Projekte/getimo/40-Plans/active/
|
|
6
|
+
2026-05-18-apimapper-mcp-implementation-plan.md` (vault, not public).
|
|
7
|
+
|
|
8
|
+
## System diagram
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
12
|
+
│ AI client │
|
|
13
|
+
│ (Claude Desktop · Claude Code · Cursor · VS Code · ChatGPT) │
|
|
14
|
+
└─────────────┬─────────────────────────────────┬──────────────┘
|
|
15
|
+
│ stdio │ HTTP+OAuth2 PKCE
|
|
16
|
+
│ (npx / DXT) │ (server-http.ts)
|
|
17
|
+
▼ ▼
|
|
18
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
19
|
+
│ @wootsup/mcp (this package) │
|
|
20
|
+
│ │
|
|
21
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
|
22
|
+
│ │ Transport │→ │ Tool │→ │ Platform router │ │
|
|
23
|
+
│ │ stdio / │ │ registry │ │ (WP / Joomla) │ │
|
|
24
|
+
│ │ HTTP+OAuth │ │ (~76 tools) │ │ │ │
|
|
25
|
+
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
|
|
26
|
+
│ │ ▲ │ │
|
|
27
|
+
│ │ │ │ │
|
|
28
|
+
│ ▼ │ ▼ │
|
|
29
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
|
30
|
+
│ │ Setup CLI │ │ Bundled │ │ HTTP client │ │
|
|
31
|
+
│ │ (clack) │ │ skill │ │ + sanitizer │ │
|
|
32
|
+
│ └──────┬───────┘ └──────────────┘ └────────┬─────────┘ │
|
|
33
|
+
│ │ keychain / profile │ │
|
|
34
|
+
│ ▼ │ │
|
|
35
|
+
│ ┌──────────────┐ │ │
|
|
36
|
+
│ │ Keychain │ │ │
|
|
37
|
+
│ │ + Profiles │ │ │
|
|
38
|
+
│ └──────────────┘ │ │
|
|
39
|
+
└────────────────────────────────────────────────┼─────────────┘
|
|
40
|
+
│
|
|
41
|
+
│ HTTPS + Bearer
|
|
42
|
+
│ (amk_live_…)
|
|
43
|
+
▼
|
|
44
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
45
|
+
│ API Mapper plugin (WordPress or Joomla) │
|
|
46
|
+
│ │
|
|
47
|
+
│ WordPress: /wp-json/api-mapper/v1/{mcp,connections,...} │
|
|
48
|
+
│ Joomla: /?option=com_apimapper&task=mcp.{...} │
|
|
49
|
+
│ │
|
|
50
|
+
│ HMAC-SHA256 verify → scope check → handler │
|
|
51
|
+
└──────────────────────────────────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Five components
|
|
55
|
+
|
|
56
|
+
1. **Transport layer** (`src/transports/`)
|
|
57
|
+
- `stdio.ts` — default; pipes JSON-RPC over stdin/stdout. Spawned
|
|
58
|
+
by the AI client.
|
|
59
|
+
- `http.ts` + `server-http.ts` — HTTP+SSE transport behind an
|
|
60
|
+
OAuth 2.0 PKCE-required authorization server. Bearer tokens are
|
|
61
|
+
site-bound; cross-origin requests are rejected.
|
|
62
|
+
|
|
63
|
+
2. **Tool registry** (`src/modules/apimapper/`)
|
|
64
|
+
- 15 register-files (connections, credentials, flows, graph,
|
|
65
|
+
local-sources, schema, library, cache, settings, license, misc,
|
|
66
|
+
workflows, get-skill, onboarding, inspect, diagnose,
|
|
67
|
+
use-profile) plus 1 entry-point (`index.ts`).
|
|
68
|
+
- Each tool follows the `@getimo/mcp-toolkit` Goldstandard:
|
|
69
|
+
annotations, structured errors, `formatResult` / `autoFormatTable`,
|
|
70
|
+
pagination clamps, destructive confirm-guards.
|
|
71
|
+
|
|
72
|
+
3. **Platform router** (`src/modules/apimapper/client.ts`)
|
|
73
|
+
- Detects WP vs. Joomla from site URL + `APIMAPPER_PLATFORM` env.
|
|
74
|
+
- Dispatches to `/wp-json/api-mapper/v1/...` (WP) or
|
|
75
|
+
`index.php?option=com_apimapper&task=...` (Joomla).
|
|
76
|
+
- Joomla envelope unwrap is handled here transparently.
|
|
77
|
+
|
|
78
|
+
4. **Auth / keychain / profiles** (`src/auth/`)
|
|
79
|
+
- `keychain.ts` — `@napi-rs/keyring` wrapper with file-fallback.
|
|
80
|
+
- `profiles.ts` — named profiles (`APIMAPPER_PROFILE=staging` ↔
|
|
81
|
+
`APIMAPPER_PROFILE=prod`).
|
|
82
|
+
- `token.ts` — `amk_*` token decoding (payload only — signature is
|
|
83
|
+
opaque, verified server-side).
|
|
84
|
+
- `credential-sanitizer.ts` — strips secrets from every response.
|
|
85
|
+
|
|
86
|
+
5. **Setup + skills** (`src/setup-cli.ts`, `skills/apimapper/`)
|
|
87
|
+
- `@clack/prompts` interactive wizard.
|
|
88
|
+
- Auto-detects AI clients, patches their MCP config idempotently.
|
|
89
|
+
- Ships the skill files (`SKILL.md` + 4 references) into
|
|
90
|
+
`~/.claude/skills/apimapper/`.
|
|
91
|
+
|
|
92
|
+
## Auth flow (token issuance)
|
|
93
|
+
|
|
94
|
+
1. User runs `npx @wootsup/mcp setup`.
|
|
95
|
+
2. Wizard asks for site URL + token.
|
|
96
|
+
3. Wizard calls `GET <site>/api-mapper/v1/mcp/identity` and verifies
|
|
97
|
+
the returned `siteSignature` against the public `key_id` carried
|
|
98
|
+
in the token payload.
|
|
99
|
+
4. User confirms the displayed site title + URL.
|
|
100
|
+
5. Wizard writes the token to OS keychain (or `0600` file fallback)
|
|
101
|
+
under a profile name (default: `default`).
|
|
102
|
+
6. Wizard patches the AI client's MCP config to launch
|
|
103
|
+
`npx -y @wootsup/mcp` with `APIMAPPER_PROFILE=<name>` in env.
|
|
104
|
+
|
|
105
|
+
At runtime, the server reads the token from the keychain on every
|
|
106
|
+
launch — tokens never live in the client config file.
|
|
107
|
+
|
|
108
|
+
## Multi-transport rationale
|
|
109
|
+
|
|
110
|
+
| | stdio | HTTP+OAuth |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| Where it runs | User's machine | User's machine or remote |
|
|
113
|
+
| Auth | Bearer from keychain | OAuth 2.0 PKCE bound to site |
|
|
114
|
+
| Use case | Local dev, single user | Remote AI agents (future) |
|
|
115
|
+
| Discovery | Static (one-tool-set) | Dynamic per session |
|
|
116
|
+
|
|
117
|
+
Both paths feed the same tool registry; the only difference is the
|
|
118
|
+
transport adapter wired into `createServer()`.
|
|
119
|
+
|
|
120
|
+
## Multi-platform rationale
|
|
121
|
+
|
|
122
|
+
The same REST contract is implemented in both the WordPress plugin
|
|
123
|
+
and the Joomla extension. The router maps tool calls to whichever
|
|
124
|
+
URL shape the user's platform speaks. This means:
|
|
125
|
+
|
|
126
|
+
- One MCP server binary, two backends.
|
|
127
|
+
- Every tool works on both platforms (parity test in
|
|
128
|
+
`apimapper_platform_parity`).
|
|
129
|
+
- Users can move a flow between WP and Joomla via
|
|
130
|
+
`apimapper_flow_export` / `apimapper_flow_import` without manual
|
|
131
|
+
rewriting.
|
|
132
|
+
|
|
133
|
+
## Further reading
|
|
134
|
+
|
|
135
|
+
- [`security.md`](./security.md) — threat model + mitigations.
|
|
136
|
+
- [`tools.md`](./tools.md) — full tool inventory.
|
|
137
|
+
- [`../skills/apimapper/SKILL.md`](../skills/apimapper/SKILL.md) —
|
|
138
|
+
task-oriented usage guide.
|
|
139
|
+
- Internal: `~/Projekte/getimo/40-Plans/active/
|
|
140
|
+
2026-05-18-apimapper-mcp-implementation-plan.md` (full design plan).
|