@kepler-project/almanac
Advanced tools
+74
-182
@@ -30,8 +30,2 @@ #!/usr/bin/env node | ||
| function authBaseUrlFromGrpcEndpoint(grpcEndpoint) { | ||
| const [host] = normalizeGrpcEndpoint(grpcEndpoint).split(":"); | ||
| const authHost = host.startsWith("grpc.") ? host.replace(/^grpc\./, "api.") : host; | ||
| return `https://${authHost}`; | ||
| } | ||
| function loadGrpcPackages() { | ||
@@ -135,50 +129,29 @@ const packageDefinition = protoLoader.loadSync( | ||
| function shouldFallbackToRest(error) { | ||
| return String(error?.message || error || "").includes("gRPC "); | ||
| } | ||
| function isNotFoundError(error) { | ||
| const message = String(error?.message || error || "").toLowerCase(); | ||
| return ( | ||
| message.includes("not_found") || | ||
| message.includes("not found") || | ||
| message.includes("(404)") | ||
| ); | ||
| } | ||
| async function fetchJson(pathname, { method = "GET", query = {}, body = undefined, auth = true, _retried = false } = {}) { | ||
| const apiBase = authBaseUrlFromGrpcEndpoint(GRPC_ENDPOINT); | ||
| const url = new URL(pathname, apiBase.endsWith("/") ? apiBase : `${apiBase}/`); | ||
| Object.entries(query).forEach(([k, v]) => { | ||
| if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v)); | ||
| }); | ||
| const headers = { "Content-Type": "application/json" }; | ||
| if (auth) headers.Authorization = `Bearer ${await getAccessToken()}`; | ||
| async function fetchAuthToken() { | ||
| // AUTH QUARANTINE: This is the only REST call. Service-token has no gRPC equivalent. | ||
| const [grpcHost] = normalizeGrpcEndpoint(GRPC_ENDPOINT).split(":"); | ||
| const authHost = grpcHost.startsWith("grpc.") ? grpcHost.replace(/^grpc\./, "api.") : grpcHost; | ||
| const url = new URL("/v1/auth/service-token", `https://${authHost}`); | ||
| const resp = await fetch(url, { | ||
| method, | ||
| headers, | ||
| body: body === undefined ? undefined : JSON.stringify(body), | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET }), | ||
| }); | ||
| if (resp.status === 401 && auth && !_retried) { | ||
| tokenCache = null; | ||
| return fetchJson(pathname, { method, query, body, auth, _retried: true }); | ||
| const txt = await resp.text(); | ||
| let parsed = {}; | ||
| if (txt) { | ||
| try { | ||
| parsed = JSON.parse(txt); | ||
| } catch { | ||
| throw new Error(`POST ${url.pathname} failed (${resp.status}): ${txt}`); | ||
| } | ||
| } | ||
| const txt = await resp.text(); | ||
| const parsed = txt ? safeJson(txt) : {}; | ||
| if (!resp.ok) throw new Error(`${method} ${url.pathname} failed (${resp.status}): ${txt}`); | ||
| if (!resp.ok) throw new Error(`POST ${url.pathname} failed (${resp.status}): ${txt}`); | ||
| return parsed; | ||
| } | ||
| function safeJson(s) { | ||
| try { return JSON.parse(s); } catch { return { raw: s }; } | ||
| } | ||
| async function getAccessToken() { | ||
| const now = Date.now(); | ||
| if (tokenCache && now < tokenExpiresAt - 60_000) return tokenCache; | ||
| const out = await fetchJson("/v1/auth/service-token", { | ||
| method: "POST", | ||
| auth: false, | ||
| body: { client_id: CLIENT_ID, client_secret: CLIENT_SECRET }, | ||
| }); | ||
| const out = await fetchAuthToken(); | ||
| tokenCache = out.access_token; | ||
@@ -189,3 +162,3 @@ tokenExpiresAt = now + ((out.expires_in || 3600) * 1000); | ||
| const server = new McpServer({ name: "kepler-almanac", version: "0.2.8" }); | ||
| const server = new McpServer({ name: "kepler-almanac", version: "0.3.0" }); | ||
@@ -210,28 +183,19 @@ function jsonResult(data) { | ||
| }, async ({ search = "", limit = 50 }) => { | ||
| const clients = await getGrpcClients(); | ||
| try { | ||
| const clients = await getGrpcClients(); | ||
| const response = await grpcUnary( | ||
| clients.communications, | ||
| "listAllCustomers", | ||
| { limit: search ? Math.max(limit * 20, 500) : limit, offset: 0 }, | ||
| ).catch((e) => { throw grpcError(e); }); | ||
| const customers = search | ||
| ? (response.customers || []) | ||
| .filter((c) => (c.name || "").toLowerCase().includes(search.toLowerCase())) | ||
| .slice(0, limit) | ||
| : (response.customers || []); | ||
| if (customers.length > 0 || !search) { | ||
| return jsonResult(search | ||
| ? { customers, count: customers.length, partialName: search } | ||
| : response); | ||
| if (search) { | ||
| const response = await grpcUnary(clients.communications, "searchCustomers", { | ||
| partialName: search, | ||
| limit, | ||
| }); | ||
| return jsonResult({ | ||
| customers: response.customers || [], | ||
| count: response.count ?? (response.customers || []).length, | ||
| partialName: search, | ||
| }); | ||
| } | ||
| } catch { | ||
| // Fall through to REST fallback for customer listing/search. | ||
| return jsonResult(await grpcUnary(clients.communications, "listAllCustomers", { limit, offset: 0 })); | ||
| } catch (error) { | ||
| throw grpcError(error); | ||
| } | ||
| const path = search ? "/v1/communications/customers/search" : "/v1/communications/customers/all"; | ||
| const query = search ? { partial_name: search, limit } : { limit }; | ||
| return jsonResult(await fetchJson(path, { query })); | ||
| }); | ||
@@ -264,3 +228,3 @@ | ||
| include: [], | ||
| }).catch((e) => { throw grpcError(e); }); | ||
| }); | ||
| return jsonResult({ | ||
@@ -271,14 +235,3 @@ results: response.results, | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson("/v1/communications/search", { | ||
| query: { | ||
| account_id: a.account_id, | ||
| customer_name: a.participant_name, | ||
| customer_names: a.customer_names?.join(","), | ||
| type: a.type, | ||
| sent_after: a.start_date, | ||
| sent_before: a.end_date, | ||
| limit: a.limit ?? 50, | ||
| }, | ||
| })); | ||
| throw grpcError(error); | ||
| } | ||
@@ -295,13 +248,8 @@ }); | ||
| const clients = await getGrpcClients(); | ||
| return jsonResult(await grpcUnary(clients.briefs, "getBrief", { briefId, enrich: false }).catch((e) => { throw grpcError(e); })); | ||
| return jsonResult(await grpcUnary(clients.briefs, "getBrief", { briefId, enrich: false })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error) && !isNotFoundError(error)) throw error; | ||
| try { | ||
| return jsonResult(await fetchJson(`/v1/briefs/${encodeURIComponent(briefId)}`)); | ||
| } catch (inner) { | ||
| if (isNotFoundError(inner)) { | ||
| return jsonResult({ status: "not_found", brief_id: briefId }); | ||
| } | ||
| throw inner; | ||
| if (error?.code === grpc.status.NOT_FOUND) { | ||
| return jsonResult({ status: "not_found", brief_id: briefId }); | ||
| } | ||
| throw grpcError(error); | ||
| } | ||
@@ -311,17 +259,14 @@ }); | ||
| server.tool("almanac_get_communication_detail", { id: z.string() }, async ({ id }) => { | ||
| const clients = await getGrpcClients(); | ||
| try { | ||
| const clients = await getGrpcClients(); | ||
| return jsonResult(await grpcUnary(clients.communications, "getCall", { callId: id }).catch((e) => { throw grpcError(e); })); | ||
| return jsonResult(await grpcUnary(clients.communications, "getCall", { callId: id })); | ||
| } catch (error) { | ||
| if (!String(error.message).includes("NOT_FOUND") && !shouldFallbackToRest(error)) throw error; | ||
| if (error?.code !== grpc.status.NOT_FOUND) throw grpcError(error); | ||
| try { | ||
| const clients = await getGrpcClients(); | ||
| return jsonResult(await grpcUnary(clients.communications, "getEmail", { emailId: id }).catch((e) => { throw grpcError(e); })); | ||
| return jsonResult(await grpcUnary(clients.communications, "getEmail", { emailId: id })); | ||
| } catch (inner) { | ||
| if (!shouldFallbackToRest(inner)) throw inner; | ||
| try { | ||
| return jsonResult(await fetchJson(`/v1/communications/calls/${encodeURIComponent(id)}`)); | ||
| } catch { | ||
| return jsonResult(await fetchJson(`/v1/communications/emails/${encodeURIComponent(id)}`)); | ||
| if (inner?.code === grpc.status.NOT_FOUND) { | ||
| return jsonResult({ status: "not_found", id }); | ||
| } | ||
| throw grpcError(inner); | ||
| } | ||
@@ -339,6 +284,5 @@ } | ||
| const clients = await getGrpcClients(); | ||
| return jsonResult(await grpcUnary(clients.knowledge, "getRawContent", { communicationId: cid }).catch((e) => { throw grpcError(e); })); | ||
| return jsonResult(await grpcUnary(clients.knowledge, "getRawContent", { communicationId: cid })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson(`/v1/communications/${encodeURIComponent(cid)}/raw-content`)); | ||
| throw grpcError(error); | ||
| } | ||
@@ -369,17 +313,5 @@ }); | ||
| enrich: false, | ||
| }).catch((e) => { throw grpcError(e); })); | ||
| })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson("/v1/communications/semantic-search", { | ||
| query: { | ||
| q: a.query, | ||
| account_ids: a.account_ids?.join(","), | ||
| customer_names: a.customer_names?.join(","), | ||
| type: a.type, | ||
| sent_after: a.sent_after, | ||
| sent_before: a.sent_before, | ||
| min_similarity: a.min_similarity, | ||
| limit: a.limit ?? 20, | ||
| }, | ||
| })); | ||
| throw grpcError(error); | ||
| } | ||
@@ -399,6 +331,5 @@ }); | ||
| limit, | ||
| }).catch((e) => { throw grpcError(e); })); | ||
| })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson("/v1/gaps/semantic-search", { query: { q: query, min_similarity, limit } })); | ||
| throw grpcError(error); | ||
| } | ||
@@ -410,6 +341,5 @@ }); | ||
| const clients = await getGrpcClients(); | ||
| return jsonResult(await grpcUnary(clients.knowledge, "getAccountGaps", { accountId: account_id, enrich: false }).catch((e) => { throw grpcError(e); })); | ||
| return jsonResult(await grpcUnary(clients.knowledge, "getAccountGaps", { accountId: account_id, enrich: false })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson(`/v1/gaps/accounts/${encodeURIComponent(account_id)}`)); | ||
| throw grpcError(error); | ||
| } | ||
@@ -421,6 +351,5 @@ }); | ||
| const clients = await getGrpcClients(); | ||
| return jsonResult(await grpcUnary(clients.knowledge, "getOpportunityGaps", { opportunityId: opportunity_id }).catch((e) => { throw grpcError(e); })); | ||
| return jsonResult(await grpcUnary(clients.knowledge, "getOpportunityGaps", { opportunityId: opportunity_id })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson(`/v1/gaps/opportunities/${encodeURIComponent(opportunity_id)}`)); | ||
| throw grpcError(error); | ||
| } | ||
@@ -442,13 +371,5 @@ }); | ||
| limit: a.limit ?? 25, | ||
| }).catch((e) => { throw grpcError(e); })); | ||
| })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson("/v1/gaps/search", { | ||
| query: { | ||
| q: a.query, | ||
| product_area: a.product_area, | ||
| min_account_count: a.min_account_count ?? 0, | ||
| limit: a.limit ?? 25, | ||
| }, | ||
| })); | ||
| throw grpcError(error); | ||
| } | ||
@@ -460,6 +381,5 @@ }); | ||
| const clients = await getGrpcClients(); | ||
| return jsonResult(await grpcUnary(clients.salesforce, "getAccount", { accountId: account_id, enrich: false }).catch((e) => { throw grpcError(e); })); | ||
| return jsonResult(await grpcUnary(clients.salesforce, "getAccount", { accountId: account_id, enrich: false })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson(`/v1/salesforce/accounts/${encodeURIComponent(account_id)}`)); | ||
| throw grpcError(error); | ||
| } | ||
@@ -485,14 +405,5 @@ }); | ||
| cursor: "", | ||
| }).catch((e) => { throw grpcError(e); })); | ||
| })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson("/v1/team/communications", { | ||
| query: { | ||
| manager_email: a.manager_email, | ||
| start_at: a.start_at, | ||
| end_at: a.end_at, | ||
| type: a.type, | ||
| limit: a.limit ?? 500, | ||
| }, | ||
| })); | ||
| throw grpcError(error); | ||
| } | ||
@@ -514,8 +425,5 @@ }); | ||
| limit, | ||
| }).catch((e) => { throw grpcError(e); })); | ||
| })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson("/v1/knowledge/confluence/search", { | ||
| query: { q: query, space_key, min_similarity, limit }, | ||
| })); | ||
| throw grpcError(error); | ||
| } | ||
@@ -533,16 +441,8 @@ }); | ||
| maxChars: max_chars ?? 0, | ||
| }).catch((e) => { throw grpcError(e); })); | ||
| })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error) && !isNotFoundError(error)) throw error; | ||
| try { | ||
| return jsonResult(await fetchJson(`/v1/knowledge/confluence/pages/${encodeURIComponent(page_id)}`, { query: { max_chars } })); | ||
| } catch (inner) { | ||
| if (isNotFoundError(inner)) { | ||
| return jsonResult({ status: "not_found", page_id }); | ||
| } | ||
| if (String(inner?.message || inner || "").includes("/v1/knowledge/confluence/pages/")) { | ||
| return jsonResult({ status: "unavailable", page_id, reason: "backend_lookup_failed" }); | ||
| } | ||
| throw inner; | ||
| if (error?.code === grpc.status.NOT_FOUND) { | ||
| return jsonResult({ status: "not_found", page_id }); | ||
| } | ||
| throw grpcError(error); | ||
| } | ||
@@ -563,8 +463,5 @@ }); | ||
| limit, | ||
| }).catch((e) => { throw grpcError(e); })); | ||
| })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error)) throw error; | ||
| return jsonResult(await fetchJson("/v1/knowledge/jira/search", { | ||
| query: { q: query, project_key, limit }, | ||
| })); | ||
| throw grpcError(error); | ||
| } | ||
@@ -579,13 +476,8 @@ }); | ||
| enrich: false, | ||
| }).catch((e) => { throw grpcError(e); })); | ||
| })); | ||
| } catch (error) { | ||
| if (!shouldFallbackToRest(error) && !isNotFoundError(error)) throw error; | ||
| try { | ||
| return jsonResult(await fetchJson(`/v1/knowledge/jira/issues/${encodeURIComponent(issue_key)}`)); | ||
| } catch (inner) { | ||
| if (isNotFoundError(inner)) { | ||
| return jsonResult({ status: "not_found", issue_key }); | ||
| } | ||
| throw inner; | ||
| if (error?.code === grpc.status.NOT_FOUND) { | ||
| return jsonResult({ status: "not_found", issue_key }); | ||
| } | ||
| throw grpcError(error); | ||
| } | ||
@@ -592,0 +484,0 @@ }); |
+1
-1
| { | ||
| "name": "@kepler-project/almanac", | ||
| "version": "0.2.11", | ||
| "version": "0.3.0", | ||
| "description": "Kepler Almanac MCP server.", | ||
@@ -5,0 +5,0 @@ "license": "UNLICENSED", |
+2
-0
@@ -25,2 +25,4 @@ # Kepler Almanac MCP Server | ||
| Tool execution in this package is gRPC-only. The only HTTP request is the auth token mint to `/v1/auth/service-token`, which has no gRPC equivalent. | ||
| ## Requirements | ||
@@ -27,0 +29,0 @@ |
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
78
2.63%38751
-11.62%434
-18.88%