@nextfreelatech/xpec-mcp 1.0.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/dist/config.js ADDED
@@ -0,0 +1,159 @@
1
+ // Config + binding resolution (per Xpec specs "mcp-server" §3+§6
2
+ // and "mcp-workspace-tools" §4). Phase 4 of the Workspaces epic introduces
3
+ // an aggregation layer above Products, so a project binding can name either
4
+ // a Workspace, a Product, or both.
5
+ //
6
+ // Precedence per binding key (workspaceId, productId):
7
+ // 1. `.xpec.json` at the project root
8
+ // 2. `XPEC_WORKSPACE_ID` / `XPEC_PRODUCT_ID` env vars
9
+ // 3. unset
10
+ //
11
+ // Effective binding mode is computed from the resolved pair:
12
+ // * both set → "workspace+product"
13
+ // * workspaceId only → "workspace"
14
+ // * productId only → "product" (orphan / pre-aggregation Product)
15
+ // * neither → "discovery" (only list_workspaces / list_products
16
+ // orphan-only are usable until ids are passed)
17
+ //
18
+ // API URL precedence:
19
+ // 1. `--api-url` command-line flag (parsed in cli.ts)
20
+ // 2. `apiUrl` in `.xpec.json`
21
+ // 3. `XPEC_API_URL` environment variable
22
+ // 4. https://app.xpec.com (default)
23
+ import { readFileSync } from "node:fs";
24
+ import { resolve as resolvePath } from "node:path";
25
+ export const DEFAULT_API_URL = "https://app.xpec.com";
26
+ export const PROJECT_CONFIG_FILENAME = ".xpec.json";
27
+ export class ConfigError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = "ConfigError";
31
+ }
32
+ }
33
+ function parseProjectFile(cwd, fileReader) {
34
+ const path = resolvePath(cwd, PROJECT_CONFIG_FILENAME);
35
+ const raw = fileReader(path);
36
+ if (raw === null)
37
+ return null;
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(raw);
41
+ }
42
+ catch (err) {
43
+ throw new ConfigError(`${PROJECT_CONFIG_FILENAME} is not valid JSON: ${err.message}`);
44
+ }
45
+ if (!parsed || typeof parsed !== "object") {
46
+ throw new ConfigError(`${PROJECT_CONFIG_FILENAME} must contain a JSON object.`);
47
+ }
48
+ return parsed;
49
+ }
50
+ function defaultFileReader(path) {
51
+ try {
52
+ return readFileSync(path, "utf8");
53
+ }
54
+ catch (err) {
55
+ if (err.code === "ENOENT")
56
+ return null;
57
+ throw err;
58
+ }
59
+ }
60
+ function normalizeApiUrl(value) {
61
+ return value.replace(/\/+$/, "");
62
+ }
63
+ export function resolveConfig(overrides = {}) {
64
+ const cwd = overrides.cwd ?? process.cwd();
65
+ const env = overrides.env ?? process.env;
66
+ const fileReader = overrides.fileReader ?? defaultFileReader;
67
+ const file = parseProjectFile(cwd, fileReader);
68
+ // workspaceId
69
+ let workspaceId = null;
70
+ let workspaceSource = "none";
71
+ if (file &&
72
+ typeof file.workspaceId === "string" &&
73
+ file.workspaceId.length > 0) {
74
+ workspaceId = file.workspaceId;
75
+ workspaceSource = "config-file";
76
+ }
77
+ else if (typeof env.XPEC_WORKSPACE_ID === "string" &&
78
+ env.XPEC_WORKSPACE_ID.length > 0) {
79
+ workspaceId = env.XPEC_WORKSPACE_ID;
80
+ workspaceSource = "env";
81
+ }
82
+ // productId
83
+ let productId = null;
84
+ let productSource = "none";
85
+ if (file && typeof file.productId === "string" && file.productId.length > 0) {
86
+ productId = file.productId;
87
+ productSource = "config-file";
88
+ }
89
+ else if (typeof env.XPEC_PRODUCT_ID === "string" &&
90
+ env.XPEC_PRODUCT_ID.length > 0) {
91
+ productId = env.XPEC_PRODUCT_ID;
92
+ productSource = "env";
93
+ }
94
+ // Phase 4 of the Workspaces epic: both ids are optional and the
95
+ // server starts in discovery mode when both are absent. The previous
96
+ // "productId required when the file is present" rule is removed; an
97
+ // empty `{}` file just means "no binding".
98
+ // apiUrl
99
+ let apiUrl = DEFAULT_API_URL;
100
+ let apiUrlSource = "default";
101
+ if (overrides.apiUrl) {
102
+ apiUrl = overrides.apiUrl;
103
+ apiUrlSource = "argument";
104
+ }
105
+ else if (file && typeof file.apiUrl === "string" && file.apiUrl.length > 0) {
106
+ apiUrl = file.apiUrl;
107
+ apiUrlSource = "config-file";
108
+ }
109
+ else if (typeof env.XPEC_API_URL === "string" &&
110
+ env.XPEC_API_URL.length > 0) {
111
+ apiUrl = env.XPEC_API_URL;
112
+ apiUrlSource = "env";
113
+ }
114
+ apiUrl = normalizeApiUrl(apiUrl);
115
+ const allowInsecure = overrides.allowInsecure ?? false;
116
+ if (!allowInsecure && !apiUrl.startsWith("https://") && !isLocalUrl(apiUrl)) {
117
+ throw new ConfigError(`apiUrl "${apiUrl}" is not HTTPS. Pass --allow-insecure to opt in (intended for self-hosted dev only).`);
118
+ }
119
+ // token
120
+ const token = typeof env.XPEC_API_TOKEN === "string" &&
121
+ env.XPEC_API_TOKEN.length > 0
122
+ ? env.XPEC_API_TOKEN
123
+ : null;
124
+ // telemetry
125
+ const telemetryEnabled = env.XPEC_TELEMETRY !== "0";
126
+ const bindingMode = workspaceId && productId
127
+ ? "workspace+product"
128
+ : workspaceId
129
+ ? "workspace"
130
+ : productId
131
+ ? "product"
132
+ : "discovery";
133
+ return {
134
+ apiUrl,
135
+ apiUrlSource,
136
+ token,
137
+ workspaceId,
138
+ workspaceSource,
139
+ productId,
140
+ productSource,
141
+ bindingMode,
142
+ telemetryEnabled,
143
+ allowInsecure,
144
+ };
145
+ }
146
+ // Localhost / loopback URLs are commonly used during dev — let `http://localhost…`
147
+ // through without `--allow-insecure`. Anything else over HTTP needs the flag.
148
+ function isLocalUrl(url) {
149
+ try {
150
+ const parsed = new URL(url);
151
+ return (parsed.hostname === "localhost" ||
152
+ parsed.hostname === "127.0.0.1" ||
153
+ parsed.hostname === "::1");
154
+ }
155
+ catch {
156
+ return false;
157
+ }
158
+ }
159
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/errors.js ADDED
@@ -0,0 +1,100 @@
1
+ // MCP-side error mapping (per Xpec spec "mcp-server" §5 "Error mapping").
2
+ // Translates Xpec API error envelopes into MCP error codes the
3
+ // agent can reason about, each carrying a short remediation string.
4
+ export class McpToolError extends Error {
5
+ code;
6
+ remediation;
7
+ constructor(code, message, remediation) {
8
+ super(message);
9
+ this.code = code;
10
+ this.remediation = remediation;
11
+ this.name = "McpToolError";
12
+ }
13
+ toFailure() {
14
+ return {
15
+ code: this.code,
16
+ message: this.message,
17
+ remediation: this.remediation,
18
+ };
19
+ }
20
+ }
21
+ const REMEDIATIONS = {
22
+ AUTH_FAILED: "Regenerate a Personal Access Token from /settings/developer and update XPEC_API_TOKEN.",
23
+ TOKEN_EXPIRED: "The token expired. Generate a new one from /settings/developer.",
24
+ TOKEN_REVOKED: "The token was revoked. Generate a new one from /settings/developer.",
25
+ TOKEN_SCOPE_MISMATCH: "The token isn't scoped to this product. Use a token whose allowlist includes it, or remove the allowlist.",
26
+ PRODUCT_NOT_BOUND: "Call list_products, pick one, then add it to .xpec.json or set XPEC_PRODUCT_ID.",
27
+ WORKSPACE_NOT_BOUND: "Call list_workspaces, pick one, then add it to .xpec.json as `workspaceId` or set XPEC_WORKSPACE_ID.",
28
+ PRODUCT_TYPE_MISMATCH: "The filter you passed isn't compatible with this product's type. Drop the filter or call against a matching product.",
29
+ NOT_IN_WORKSPACE: "This tool requires a Workspace binding. Set `workspaceId` in .xpec.json or pass it explicitly.",
30
+ LEGACY_BINDING_DETECTED: 'Edit .xpec.json: rename the "workspaceId" field to "productId" (the value points at a Product under the new model). To bind to a Workspace, create one and set both ids.',
31
+ SPEC_LOCKED: "The spec is in REVIEWED state. Call start_new_version first to enter Draft.",
32
+ OPEN_QUESTIONS_PRESENT: "Resolve or dismiss the open questions on this spec before proceeding.",
33
+ STALE_VERSION: "Re-read the spec to pick up the current OCC version, then retry with that version.",
34
+ RATE_LIMITED: "You hit the per-token rate limit. Wait until the Retry-After window passes and try again.",
35
+ NOT_FOUND: "The resource doesn't exist or isn't visible to this token.",
36
+ VALIDATION_ERROR: "Inspect the details — at least one argument failed schema validation.",
37
+ INTERNAL_ERROR: "The Xpec API hit an unexpected error. Retry once; if it persists, contact support.",
38
+ };
39
+ /**
40
+ * Map an HTTP response (status + parsed body) onto an `McpToolError`. The
41
+ * body's `error.code` takes precedence when present, falling back to the
42
+ * status code's standard meaning.
43
+ */
44
+ export function mapApiError(status, body) {
45
+ const apiCode = body?.error?.code;
46
+ const apiMessage = body?.error?.message ?? `Request failed with status ${status}.`;
47
+ const code = pickStructuredCode(status, apiCode, body?.error?.details);
48
+ return new McpToolError(code, apiMessage, REMEDIATIONS[code]);
49
+ }
50
+ function pickStructuredCode(status, apiCode, details) {
51
+ // Prefer explicit codes on the API error envelope or in `details.code`.
52
+ const explicitCode = apiCode ?? extractDetailCode(details);
53
+ if (explicitCode) {
54
+ if (explicitCode === "TOKEN_SCOPE_MISMATCH")
55
+ return "TOKEN_SCOPE_MISMATCH";
56
+ if (explicitCode === "TOKEN_EXPIRED")
57
+ return "TOKEN_EXPIRED";
58
+ if (explicitCode === "TOKEN_REVOKED")
59
+ return "TOKEN_REVOKED";
60
+ if (explicitCode === "PRODUCT_TYPE_MISMATCH")
61
+ return "PRODUCT_TYPE_MISMATCH";
62
+ if (explicitCode === "NOT_IN_WORKSPACE")
63
+ return "NOT_IN_WORKSPACE";
64
+ if (explicitCode === "SPEC_LOCKED")
65
+ return "SPEC_LOCKED";
66
+ if (explicitCode === "OPEN_QUESTIONS_PRESENT")
67
+ return "OPEN_QUESTIONS_PRESENT";
68
+ if (explicitCode === "STALE_VERSION")
69
+ return "STALE_VERSION";
70
+ if (explicitCode === "AUTH_REQUIRED" || explicitCode === "AUTH_FAILED")
71
+ return "AUTH_FAILED";
72
+ }
73
+ // Fallback by status code.
74
+ if (status === 401)
75
+ return "AUTH_FAILED";
76
+ if (status === 403)
77
+ return "TOKEN_SCOPE_MISMATCH";
78
+ if (status === 404)
79
+ return "NOT_FOUND";
80
+ if (status === 409)
81
+ return "STALE_VERSION";
82
+ if (status === 422)
83
+ return "VALIDATION_ERROR";
84
+ if (status === 429)
85
+ return "RATE_LIMITED";
86
+ return "INTERNAL_ERROR";
87
+ }
88
+ function extractDetailCode(details) {
89
+ if (details &&
90
+ typeof details === "object" &&
91
+ "code" in details &&
92
+ typeof details.code === "string") {
93
+ return details.code;
94
+ }
95
+ return undefined;
96
+ }
97
+ export function buildClientFailure(code, message) {
98
+ return { code, message, remediation: REMEDIATIONS[code] };
99
+ }
100
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,+DAA+D;AAC/D,oEAAoE;AAkCpE,MAAM,OAAO,YAAa,SAAQ,KAAK;IAEnB;IAEA;IAHlB,YACkB,IAAuB,EACvC,OAAe,EACC,WAAmB;QAEnC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,SAAI,GAAJ,IAAI,CAAmB;QAEvB,gBAAW,GAAX,WAAW,CAAQ;QAGnC,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;IAED,SAAS;QACP,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CAAC;IACJ,CAAC;CACF;AAED,MAAM,YAAY,GAAsC;IACtD,WAAW,EACT,wFAAwF;IAC1F,aAAa,EACX,iEAAiE;IACnE,aAAa,EACX,qEAAqE;IACvE,oBAAoB,EAClB,2GAA2G;IAC7G,iBAAiB,EACf,iFAAiF;IACnF,mBAAmB,EACjB,sGAAsG;IACxG,qBAAqB,EACnB,sHAAsH;IACxH,gBAAgB,EACd,gGAAgG;IAClG,uBAAuB,EACrB,0KAA0K;IAC5K,WAAW,EACT,6EAA6E;IAC/E,sBAAsB,EACpB,uEAAuE;IACzE,aAAa,EACX,oFAAoF;IACtF,YAAY,EACV,2FAA2F;IAC7F,SAAS,EACP,4DAA4D;IAC9D,gBAAgB,EACd,uEAAuE;IACzE,cAAc,EACZ,oFAAoF;CACvF,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,MAAc,EACd,IAAyB;IAEzB,MAAM,OAAO,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC;IAClC,MAAM,UAAU,GACd,IAAI,EAAE,KAAK,EAAE,OAAO,IAAI,8BAA8B,MAAM,GAAG,CAAC;IAClE,MAAM,IAAI,GAAG,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACvE,OAAO,IAAI,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,kBAAkB,CACzB,MAAc,EACd,OAA2B,EAC3B,OAAgB;IAEhB,wEAAwE;IACxE,MAAM,YAAY,GAAG,OAAO,IAAI,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC3D,IAAI,YAAY,EAAE,CAAC;QACjB,IAAI,YAAY,KAAK,sBAAsB;YAAE,OAAO,sBAAsB,CAAC;QAC3E,IAAI,YAAY,KAAK,eAAe;YAAE,OAAO,eAAe,CAAC;QAC7D,IAAI,YAAY,KAAK,eAAe;YAAE,OAAO,eAAe,CAAC;QAC7D,IAAI,YAAY,KAAK,uBAAuB;YAC1C,OAAO,uBAAuB,CAAC;QACjC,IAAI,YAAY,KAAK,kBAAkB;YAAE,OAAO,kBAAkB,CAAC;QACnE,IAAI,YAAY,KAAK,aAAa;YAAE,OAAO,aAAa,CAAC;QACzD,IAAI,YAAY,KAAK,wBAAwB;YAC3C,OAAO,wBAAwB,CAAC;QAClC,IAAI,YAAY,KAAK,eAAe;YAAE,OAAO,eAAe,CAAC;QAC7D,IAAI,YAAY,KAAK,eAAe,IAAI,YAAY,KAAK,aAAa;YACpE,OAAO,aAAa,CAAC;IACzB,CAAC;IAED,2BAA2B;IAC3B,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,aAAa,CAAC;IACzC,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,sBAAsB,CAAC;IAClD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,WAAW,CAAC;IACvC,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,eAAe,CAAC;IAC3C,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,kBAAkB,CAAC;IAC9C,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,cAAc,CAAC;IAC1C,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,SAAS,iBAAiB,CAAC,OAAgB;IACzC,IACE,OAAO;QACP,OAAO,OAAO,KAAK,QAAQ;QAC3B,MAAM,IAAI,OAAO;QACjB,OAAQ,OAA6B,CAAC,IAAI,KAAK,QAAQ,EACvD,CAAC;QACD,OAAQ,OAA4B,CAAC,IAAI,CAAC;IAC5C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAuB,EAAE,OAAe;IACzE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;AAC5D,CAAC"}
package/dist/http.js ADDED
@@ -0,0 +1,147 @@
1
+ // HTTP/SSE transport (per Xpec spec "mcp-server" §3 BDD
2
+ // "HTTP+SSE transport for hosted agents" + §5 "HTTP/SSE transport").
3
+ //
4
+ // This is the hosted-agent path. The MCP SDK's StreamableHTTPServerTransport
5
+ // auto-detects whether the caller wants SSE streaming or a direct JSON
6
+ // response based on the Accept header, so a single `/mcp` endpoint covers
7
+ // both shapes — that's intentional in the modern MCP wire format.
8
+ //
9
+ // CORS defaults to deny: only same-origin requests (no Origin header) and
10
+ // origins explicitly listed via `--cors-origin` are allowed. This keeps an
11
+ // agent that picks up the local server URL out of reach for a malicious
12
+ // page that happens to be open in the user's browser.
13
+ import { createServer } from "node:http";
14
+ import { randomUUID } from "node:crypto";
15
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
16
+ import { logger } from "./logger.js";
17
+ import { buildServer } from "./server.js";
18
+ export const DEFAULT_HTTP_PORT = 7345;
19
+ export const DEFAULT_HTTP_HOST = "127.0.0.1";
20
+ const MCP_PATH = "/mcp";
21
+ const HEALTH_PATH = "/healthz";
22
+ /**
23
+ * Start the HTTP/SSE MCP server. Resolves once the listener is bound so the
24
+ * caller knows the actual port (useful when port=0 picks an ephemeral port
25
+ * during tests).
26
+ */
27
+ export async function startHttpServer(options) {
28
+ const port = options.port ?? DEFAULT_HTTP_PORT;
29
+ const host = options.host ?? DEFAULT_HTTP_HOST;
30
+ const allowedOrigins = options.corsOrigins ?? [];
31
+ const mcpServer = buildServer({ config: options.config });
32
+ // Stateful mode — the transport keeps an MCP session alive across
33
+ // multiple requests so the agent's `initialize` handshake is honoured
34
+ // for every follow-up tool/resource call.
35
+ const transport = new StreamableHTTPServerTransport({
36
+ sessionIdGenerator: () => randomUUID(),
37
+ });
38
+ const server = createServer(async (req, res) => {
39
+ const url = req.url ?? "/";
40
+ const method = req.method ?? "GET";
41
+ // CORS preflight handling — applied to every request before dispatch.
42
+ if (!applyCors(req, res, allowedOrigins)) {
43
+ // applyCors already wrote the rejection response.
44
+ return;
45
+ }
46
+ if (method === "OPTIONS") {
47
+ res.statusCode = 204;
48
+ res.end();
49
+ return;
50
+ }
51
+ if (url === HEALTH_PATH && method === "GET") {
52
+ res.statusCode = 200;
53
+ res.setHeader("content-type", "application/json");
54
+ res.end(JSON.stringify({ ok: true }));
55
+ return;
56
+ }
57
+ if (url === MCP_PATH || url.startsWith(`${MCP_PATH}?`)) {
58
+ try {
59
+ await transport.handleRequest(req, res);
60
+ }
61
+ catch (err) {
62
+ logger.error("http transport handleRequest failed", {
63
+ err: err.message,
64
+ });
65
+ if (!res.headersSent) {
66
+ res.statusCode = 500;
67
+ res.setHeader("content-type", "application/json");
68
+ res.end(JSON.stringify({ error: { code: "INTERNAL_ERROR" } }));
69
+ }
70
+ }
71
+ return;
72
+ }
73
+ res.statusCode = 404;
74
+ res.setHeader("content-type", "application/json");
75
+ res.end(JSON.stringify({ error: { code: "NOT_FOUND" } }));
76
+ });
77
+ await mcpServer.connect(transport);
78
+ return new Promise((resolve, reject) => {
79
+ server.once("error", reject);
80
+ server.listen(port, host, () => {
81
+ const address = server.address();
82
+ const boundPort = address && typeof address === "object" ? address.port : port;
83
+ logger.info("mcp server connected", {
84
+ transport: "http",
85
+ host,
86
+ port: boundPort,
87
+ apiUrl: options.config.apiUrl,
88
+ apiUrlSource: options.config.apiUrlSource,
89
+ productId: options.config.productId ?? undefined,
90
+ corsOrigins: allowedOrigins,
91
+ });
92
+ if (allowedOrigins.includes("*")) {
93
+ logger.warn("CORS origin '*' is permissive — restrict for production");
94
+ }
95
+ resolve({
96
+ port: boundPort,
97
+ host,
98
+ close: () => new Promise((resolveClose, rejectClose) => {
99
+ server.close((err) => (err ? rejectClose(err) : resolveClose()));
100
+ }),
101
+ });
102
+ });
103
+ });
104
+ }
105
+ // ──────────────────────────────────────────────────────────────────────────
106
+ // CORS
107
+ // ──────────────────────────────────────────────────────────────────────────
108
+ /**
109
+ * Returns true when the request is permitted to proceed. Writes the 403
110
+ * response itself when the origin is rejected so the caller can early-exit.
111
+ *
112
+ * Exported for unit tests so the policy can be evaluated without standing
113
+ * up an actual HTTP server.
114
+ */
115
+ export function applyCors(req, res, allowedOrigins) {
116
+ const origin = req.headers.origin;
117
+ // Same-origin / non-browser request: no Origin header at all. Always
118
+ // allowed — the browser only attaches Origin to cross-origin fetches.
119
+ if (!origin)
120
+ return true;
121
+ if (allowedOrigins.includes("*")) {
122
+ res.setHeader("access-control-allow-origin", "*");
123
+ setStandardCorsHeaders(res);
124
+ return true;
125
+ }
126
+ if (allowedOrigins.includes(origin)) {
127
+ res.setHeader("access-control-allow-origin", origin);
128
+ res.setHeader("vary", "Origin");
129
+ setStandardCorsHeaders(res);
130
+ return true;
131
+ }
132
+ res.statusCode = 403;
133
+ res.setHeader("content-type", "application/json");
134
+ res.end(JSON.stringify({
135
+ error: {
136
+ code: "CORS_ORIGIN_NOT_ALLOWED",
137
+ message: `Origin "${origin}" is not in the CORS allowlist.`,
138
+ },
139
+ }));
140
+ return false;
141
+ }
142
+ function setStandardCorsHeaders(res) {
143
+ res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
144
+ res.setHeader("access-control-allow-headers", "authorization, content-type, mcp-session-id, mcp-protocol-version, last-event-id");
145
+ res.setHeader("access-control-expose-headers", "mcp-session-id, etag");
146
+ }
147
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // Public entry for the @nextfreelatech/xpec-mcp package. Most users invoke the
2
+ // CLI via `npx -y @nextfreelatech/xpec-mcp`; this module exists so library
3
+ // consumers (e.g. an integration test or an embedded MCP gateway) can
4
+ // build the same server programmatically.
5
+ export { buildServer, runStdio } from "./server.js";
6
+ export { resolveConfig, ConfigError, DEFAULT_API_URL } from "./config.js";
7
+ export { XpecClient, } from "./client.js";
8
+ export { McpToolError, mapApiError, buildClientFailure, } from "./errors.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,2EAA2E;AAC3E,sEAAsE;AACtE,0CAA0C;AAE1C,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EACL,UAAU,GAGX,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,kBAAkB,GAGnB,MAAM,aAAa,CAAC"}
package/dist/logger.js ADDED
@@ -0,0 +1,29 @@
1
+ // Structured stderr logger for the MCP server. Stderr (not stdout) because
2
+ // stdio transport reserves stdout for JSON-RPC frames — anything else there
3
+ // breaks the agent's MCP client. One JSON object per line so the agent's
4
+ // parent process can parse logs without a regex.
5
+ const LEVEL_RANK = {
6
+ debug: 10,
7
+ info: 20,
8
+ warn: 30,
9
+ error: 40,
10
+ };
11
+ const minLevel = process.env.XPEC_LOG_LEVEL ?? "info";
12
+ function emit(level, message, context = {}) {
13
+ if (LEVEL_RANK[level] < LEVEL_RANK[minLevel])
14
+ return;
15
+ const line = JSON.stringify({
16
+ t: new Date().toISOString(),
17
+ level,
18
+ msg: message,
19
+ ...context,
20
+ });
21
+ process.stderr.write(`${line}\n`);
22
+ }
23
+ export const logger = {
24
+ debug: (message, ctx) => emit("debug", message, ctx),
25
+ info: (message, ctx) => emit("info", message, ctx),
26
+ warn: (message, ctx) => emit("warn", message, ctx),
27
+ error: (message, ctx) => emit("error", message, ctx),
28
+ };
29
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,4EAA4E;AAC5E,yEAAyE;AACzE,iDAAiD;AAgBjD,MAAM,UAAU,GAA6B;IAC3C,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,EAAE;IACR,KAAK,EAAE,EAAE;CACV,CAAC;AAEF,MAAM,QAAQ,GACX,OAAO,CAAC,GAAG,CAAC,cAAuC,IAAI,MAAM,CAAC;AAEjE,SAAS,IAAI,CAAC,KAAe,EAAE,OAAe,EAAE,UAAsB,EAAE;IACtE,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO;IACrD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;QAC1B,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAC3B,KAAK;QACL,GAAG,EAAE,OAAO;QACZ,GAAG,OAAO;KACX,CAAC,CAAC;IACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,EAAE,CAAC,OAAe,EAAE,GAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC;IACzE,IAAI,EAAE,CAAC,OAAe,EAAE,GAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC;IACvE,IAAI,EAAE,CAAC,OAAe,EAAE,GAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC;IACvE,KAAK,EAAE,CAAC,OAAe,EAAE,GAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC;CAC1E,CAAC"}
@@ -0,0 +1,181 @@
1
+ // Resource provider for the MCP server. Phase 4 doubles the surface so
2
+ // agents can address either a Product or a Workspace via stable URIs:
3
+ //
4
+ // xpec://product/{productId} — collection: lists Product specs
5
+ // xpec://product/{productId}/spec/{specId} — Product spec Markdown body
6
+ // xpec://workspace/{workspaceId} — collection: lists Workspace specs
7
+ // xpec://workspace/{workspaceId}/spec/{specId} — Workspace spec Markdown body
8
+ //
9
+ // Collection URIs are registered as fixed resources so the agent can
10
+ // list-then-pin. Individual specs are exposed via ResourceTemplates so the
11
+ // agent can read any spec in the bound scope by id. ETag/304 behavior is
12
+ // unchanged (per mcp-workspace-tools.md §3).
13
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { McpToolError } from "./errors.js";
15
+ import { logger } from "./logger.js";
16
+ export function registerResources(server, deps) {
17
+ const { client, config } = deps;
18
+ if (!config.productId && !config.workspaceId) {
19
+ // Discovery mode — no bound scope means no resource surface. The agent
20
+ // can call list_products / list_workspaces to discover ids; resources
21
+ // become available after the user binds a scope and reconnects.
22
+ logger.debug("resources skipped — no binding (discovery mode)");
23
+ return;
24
+ }
25
+ if (config.workspaceId) {
26
+ registerWorkspaceResources(server, client, config.workspaceId);
27
+ }
28
+ if (config.productId) {
29
+ registerProductResources(server, client, config.productId);
30
+ }
31
+ }
32
+ function registerProductResources(server, client, productId) {
33
+ const wsId = productId;
34
+ const collectionUri = `xpec://product/${wsId}`;
35
+ server.registerResource("product-specs", collectionUri, {
36
+ title: "Product specifications",
37
+ description: `Specifications in product ${wsId}. Read this resource to enumerate the specs an agent can pin into context.`,
38
+ mimeType: "application/json",
39
+ }, async (uri) => {
40
+ try {
41
+ const res = await client.listSpecifications(wsId, { limit: 100 });
42
+ return {
43
+ contents: [
44
+ {
45
+ uri: uri.toString(),
46
+ mimeType: "application/json",
47
+ text: JSON.stringify(res.body, null, 2),
48
+ },
49
+ ],
50
+ };
51
+ }
52
+ catch (err) {
53
+ throw mapResourceError(err);
54
+ }
55
+ });
56
+ // Per-spec template — the agent reads a Markdown body by spec id.
57
+ // The template's `{specId}` slot maps onto a string variable.
58
+ const template = new ResourceTemplate(`xpec://product/${wsId}/spec/{specId}`, {
59
+ list: undefined,
60
+ });
61
+ server.registerResource("specification-body", template, {
62
+ title: "Specification Markdown",
63
+ description: "Read the current Markdown body of a specification. The resource etag mirrors the spec's optimistic-concurrency version, so the client can re-read after change notifications without comparing content.",
64
+ mimeType: "text/markdown",
65
+ }, async (uri, variables) => {
66
+ const specId = String(variables.specId ?? "");
67
+ if (!specId) {
68
+ throw new McpToolError("VALIDATION_ERROR", "Resource URI is missing a specId.", "Use the URI shape xpec://product/{productId}/spec/{specId}.");
69
+ }
70
+ try {
71
+ const res = await client.readSpecification(specId, { format: "raw" });
72
+ if ("notModified" in res) {
73
+ // Tool reads don't carry If-None-Match, so 304 should not occur.
74
+ return {
75
+ contents: [
76
+ {
77
+ uri: uri.toString(),
78
+ mimeType: "text/markdown",
79
+ text: "",
80
+ },
81
+ ],
82
+ };
83
+ }
84
+ const body = res.body;
85
+ const text = typeof body?.content === "string" ? body.content : "";
86
+ return {
87
+ contents: [
88
+ {
89
+ uri: uri.toString(),
90
+ mimeType: "text/markdown",
91
+ text,
92
+ },
93
+ ],
94
+ // SDK passes through `_meta` on the resource result; the etag is
95
+ // surfaced for clients that want to short-circuit re-reads.
96
+ _meta: {
97
+ etag: res.etag ?? body?.etag ?? null,
98
+ },
99
+ };
100
+ }
101
+ catch (err) {
102
+ throw mapResourceError(err);
103
+ }
104
+ });
105
+ }
106
+ function registerWorkspaceResources(server, client, workspaceId) {
107
+ const collectionUri = `xpec://workspace/${workspaceId}`;
108
+ server.registerResource("workspace-specs", collectionUri, {
109
+ title: "Workspace specifications",
110
+ description: `Specifications in workspace ${workspaceId}. Read this resource to enumerate the cross-product specs an agent can pin into context.`,
111
+ mimeType: "application/json",
112
+ }, async (uri) => {
113
+ try {
114
+ const res = await client.listSpecificationsForWorkspace(workspaceId, {
115
+ limit: 100,
116
+ });
117
+ return {
118
+ contents: [
119
+ {
120
+ uri: uri.toString(),
121
+ mimeType: "application/json",
122
+ text: JSON.stringify(res.body, null, 2),
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ catch (err) {
128
+ throw mapResourceError(err);
129
+ }
130
+ });
131
+ const template = new ResourceTemplate(`xpec://workspace/${workspaceId}/spec/{specId}`, {
132
+ list: undefined,
133
+ });
134
+ server.registerResource("workspace-specification-body", template, {
135
+ title: "Workspace specification Markdown",
136
+ description: "Read the current Markdown body of a workspace-scoped specification. Etag mirrors the spec's OCC version (Phase 4 mcp-workspace-tools.md §3).",
137
+ mimeType: "text/markdown",
138
+ }, async (uri, variables) => {
139
+ const specId = String(variables.specId ?? "");
140
+ if (!specId) {
141
+ throw new McpToolError("VALIDATION_ERROR", "Resource URI is missing a specId.", "Use the URI shape xpec://workspace/{workspaceId}/spec/{specId}.");
142
+ }
143
+ try {
144
+ const res = await client.readSpecification(specId, { format: "raw" });
145
+ if ("notModified" in res) {
146
+ return {
147
+ contents: [
148
+ {
149
+ uri: uri.toString(),
150
+ mimeType: "text/markdown",
151
+ text: "",
152
+ },
153
+ ],
154
+ };
155
+ }
156
+ const body = res.body;
157
+ const text = typeof body?.content === "string" ? body.content : "";
158
+ return {
159
+ contents: [
160
+ {
161
+ uri: uri.toString(),
162
+ mimeType: "text/markdown",
163
+ text,
164
+ },
165
+ ],
166
+ _meta: {
167
+ etag: res.etag ?? body?.etag ?? null,
168
+ },
169
+ };
170
+ }
171
+ catch (err) {
172
+ throw mapResourceError(err);
173
+ }
174
+ });
175
+ }
176
+ function mapResourceError(err) {
177
+ if (err instanceof McpToolError)
178
+ return err;
179
+ return new McpToolError("INTERNAL_ERROR", err?.message ?? "Resource read failed.", "Retry once; if the failure persists, check the MCP server logs.");
180
+ }
181
+ //# sourceMappingURL=resources.js.map