@kynver-app/mcp-estimator 0.1.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.
Files changed (52) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.js +11 -0
  3. package/dist/lib/kynver-client.d.ts +20 -0
  4. package/dist/lib/kynver-client.js +110 -0
  5. package/dist/lib/mcp-sse-server.d.ts +29 -0
  6. package/dist/lib/mcp-sse-server.js +191 -0
  7. package/dist/server.d.ts +6 -0
  8. package/dist/server.js +78 -0
  9. package/dist/sse.d.ts +1 -0
  10. package/dist/sse.js +9 -0
  11. package/dist/tools/catalog-tools.d.ts +387 -0
  12. package/dist/tools/catalog-tools.js +193 -0
  13. package/dist/tools/client-tools.d.ts +193 -0
  14. package/dist/tools/client-tools.js +105 -0
  15. package/dist/tools/company-tools.d.ts +163 -0
  16. package/dist/tools/company-tools.js +98 -0
  17. package/dist/tools/create-estimate.d.ts +107 -0
  18. package/dist/tools/create-estimate.js +64 -0
  19. package/dist/tools/estimate-tools.d.ts +531 -0
  20. package/dist/tools/estimate-tools.js +259 -0
  21. package/dist/tools/estimator-chat.d.ts +25 -0
  22. package/dist/tools/estimator-chat.js +15 -0
  23. package/dist/tools/get-estimate.d.ts +19 -0
  24. package/dist/tools/get-estimate.js +14 -0
  25. package/dist/tools/list-catalog.d.ts +25 -0
  26. package/dist/tools/list-catalog.js +22 -0
  27. package/dist/tools/photo-tools.d.ts +77 -0
  28. package/dist/tools/photo-tools.js +51 -0
  29. package/dist/tools/stats-pdf-tools.d.ts +113 -0
  30. package/dist/tools/stats-pdf-tools.js +73 -0
  31. package/dist/tools/template-tools.d.ts +96 -0
  32. package/dist/tools/template-tools.js +70 -0
  33. package/dist/tools/version-tools.d.ts +46 -0
  34. package/dist/tools/version-tools.js +33 -0
  35. package/dist/tools/vision-tools.d.ts +52 -0
  36. package/dist/tools/vision-tools.js +42 -0
  37. package/package.json +27 -0
  38. package/src/index.ts +13 -0
  39. package/src/lib/kynver-client.ts +136 -0
  40. package/src/lib/mcp-sse-server.ts +249 -0
  41. package/src/server.ts +101 -0
  42. package/src/sse.ts +10 -0
  43. package/src/tools/catalog-tools.ts +217 -0
  44. package/src/tools/client-tools.ts +114 -0
  45. package/src/tools/company-tools.ts +108 -0
  46. package/src/tools/estimate-tools.ts +281 -0
  47. package/src/tools/photo-tools.ts +60 -0
  48. package/src/tools/stats-pdf-tools.ts +80 -0
  49. package/src/tools/template-tools.ts +80 -0
  50. package/src/tools/version-tools.ts +39 -0
  51. package/src/tools/vision-tools.ts +48 -0
  52. package/tsconfig.json +15 -0
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ import * as client from "../lib/kynver-client.js";
3
+ // --- estimator_get_stats ---
4
+ const getStatsSchema = z.object({
5
+ from: z.string().optional(),
6
+ to: z.string().optional(),
7
+ });
8
+ export const getStats = {
9
+ name: "estimator_get_stats",
10
+ description: "Get aggregate statistics for the user's estimating business. Returns: total estimates count, estimates by status, total revenue (accepted estimates), average estimate value, estimates by type (replacement vs repair). Optionally filter by date range (from/to as ISO strings). Use this for dashboard data and business reporting.",
11
+ inputSchemaZod: getStatsSchema,
12
+ execute: async (args) => {
13
+ if (!client.hasApiKey())
14
+ return client.noApiKeyMessage();
15
+ const params = new URLSearchParams();
16
+ if (args.from)
17
+ params.set("from", args.from);
18
+ if (args.to)
19
+ params.set("to", args.to);
20
+ const q = params.toString();
21
+ const path = q ? `/estimator/stats?${q}` : "/estimator/stats";
22
+ const data = await client.get(path);
23
+ return JSON.stringify(data);
24
+ },
25
+ };
26
+ // --- estimator_export_pdf ---
27
+ const exportPdfSchema = z.object({
28
+ estimateId: z.string(),
29
+ });
30
+ export const exportPdf = {
31
+ name: "estimator_export_pdf",
32
+ description: "Generate a PDF document for an estimate. Returns a download URL (not the binary file). The PDF layout is controlled by the estimate's output config — use estimator_update_output_config to customize which columns, sections, and details appear. The PDF includes company branding from estimator config.",
33
+ inputSchemaZod: exportPdfSchema,
34
+ execute: async (args) => {
35
+ if (!client.hasApiKey())
36
+ return client.noApiKeyMessage();
37
+ // Trigger PDF generation on the server side
38
+ try {
39
+ await client.get(`/estimator/estimates/${args.estimateId}/pdf`);
40
+ }
41
+ catch {
42
+ // Even if the binary response fails to parse, the PDF was generated
43
+ }
44
+ return JSON.stringify({
45
+ message: "PDF generated successfully.",
46
+ downloadUrl: `/api/estimator/estimates/${args.estimateId}/pdf`,
47
+ });
48
+ },
49
+ };
50
+ // --- estimator_update_output_config ---
51
+ const updateOutputConfigSchema = z.object({
52
+ estimateId: z.string(),
53
+ showMaterialCost: z.boolean().optional(),
54
+ showLaborCost: z.boolean().optional(),
55
+ showUnitPrice: z.boolean().optional(),
56
+ showQuantity: z.boolean().optional(),
57
+ showNotes: z.boolean().optional(),
58
+ headerText: z.string().optional(),
59
+ footerText: z.string().optional(),
60
+ template: z.string().optional(),
61
+ });
62
+ export const updateOutputConfig = {
63
+ name: "estimator_update_output_config",
64
+ description: "Configure what appears in an estimate's PDF output. Toggle visibility of: material costs, labor costs, unit prices, quantities, line item notes. Set custom header/footer text. Choose a template style. This controls the presentation layer — the underlying estimate data is not changed.",
65
+ inputSchemaZod: updateOutputConfigSchema,
66
+ execute: async (args) => {
67
+ if (!client.hasApiKey())
68
+ return client.noApiKeyMessage();
69
+ const { estimateId, ...body } = args;
70
+ const data = await client.patch(`/estimator/estimates/${estimateId}/output-config`, body);
71
+ return JSON.stringify(data);
72
+ },
73
+ };
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+ declare const saveTemplateSchema: z.ZodObject<{
3
+ estimateId: z.ZodString;
4
+ name: z.ZodString;
5
+ }, "strip", z.ZodTypeAny, {
6
+ name: string;
7
+ estimateId: string;
8
+ }, {
9
+ name: string;
10
+ estimateId: string;
11
+ }>;
12
+ export declare const saveTemplate: {
13
+ name: string;
14
+ description: string;
15
+ inputSchemaZod: z.ZodObject<{
16
+ estimateId: z.ZodString;
17
+ name: z.ZodString;
18
+ }, "strip", z.ZodTypeAny, {
19
+ name: string;
20
+ estimateId: string;
21
+ }, {
22
+ name: string;
23
+ estimateId: string;
24
+ }>;
25
+ execute: (args: z.infer<typeof saveTemplateSchema>) => Promise<string>;
26
+ };
27
+ declare const listTemplatesSchema: z.ZodObject<{
28
+ type: z.ZodOptional<z.ZodString>;
29
+ }, "strip", z.ZodTypeAny, {
30
+ type?: string | undefined;
31
+ }, {
32
+ type?: string | undefined;
33
+ }>;
34
+ export declare const listTemplates: {
35
+ name: string;
36
+ description: string;
37
+ inputSchemaZod: z.ZodObject<{
38
+ type: z.ZodOptional<z.ZodString>;
39
+ }, "strip", z.ZodTypeAny, {
40
+ type?: string | undefined;
41
+ }, {
42
+ type?: string | undefined;
43
+ }>;
44
+ execute: (args: z.infer<typeof listTemplatesSchema>) => Promise<string>;
45
+ };
46
+ declare const deleteTemplateSchema: z.ZodObject<{
47
+ id: z.ZodString;
48
+ }, "strip", z.ZodTypeAny, {
49
+ id: string;
50
+ }, {
51
+ id: string;
52
+ }>;
53
+ export declare const deleteTemplate: {
54
+ name: string;
55
+ description: string;
56
+ inputSchemaZod: z.ZodObject<{
57
+ id: z.ZodString;
58
+ }, "strip", z.ZodTypeAny, {
59
+ id: string;
60
+ }, {
61
+ id: string;
62
+ }>;
63
+ execute: (args: z.infer<typeof deleteTemplateSchema>) => Promise<string>;
64
+ };
65
+ declare const createFromTemplateSchema: z.ZodObject<{
66
+ templateId: z.ZodString;
67
+ projectName: z.ZodString;
68
+ clientId: z.ZodOptional<z.ZodString>;
69
+ }, "strip", z.ZodTypeAny, {
70
+ templateId: string;
71
+ projectName: string;
72
+ clientId?: string | undefined;
73
+ }, {
74
+ templateId: string;
75
+ projectName: string;
76
+ clientId?: string | undefined;
77
+ }>;
78
+ export declare const createFromTemplate: {
79
+ name: string;
80
+ description: string;
81
+ inputSchemaZod: z.ZodObject<{
82
+ templateId: z.ZodString;
83
+ projectName: z.ZodString;
84
+ clientId: z.ZodOptional<z.ZodString>;
85
+ }, "strip", z.ZodTypeAny, {
86
+ templateId: string;
87
+ projectName: string;
88
+ clientId?: string | undefined;
89
+ }, {
90
+ templateId: string;
91
+ projectName: string;
92
+ clientId?: string | undefined;
93
+ }>;
94
+ execute: (args: z.infer<typeof createFromTemplateSchema>) => Promise<string>;
95
+ };
96
+ export {};
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import * as client from "../lib/kynver-client.js";
3
+ // --- estimator_save_template ---
4
+ const saveTemplateSchema = z.object({
5
+ estimateId: z.string(),
6
+ name: z.string(),
7
+ });
8
+ export const saveTemplate = {
9
+ name: "estimator_save_template",
10
+ description: "Save an existing estimate as a reusable template. Templates capture the estimate's line items, quantities, and structure so you can quickly create similar estimates in the future. Requires the estimateId of the source estimate and a name for the template.",
11
+ inputSchemaZod: saveTemplateSchema,
12
+ execute: async (args) => {
13
+ if (!client.hasApiKey())
14
+ return client.noApiKeyMessage();
15
+ const data = await client.post("/estimator/templates", args);
16
+ return JSON.stringify(data);
17
+ },
18
+ };
19
+ // --- estimator_list_templates ---
20
+ const listTemplatesSchema = z.object({
21
+ type: z.string().optional(),
22
+ });
23
+ export const listTemplates = {
24
+ name: "estimator_list_templates",
25
+ description: "List all saved estimate templates. Optionally filter by type: 'replacement' or 'repair_service'. Returns template names, types, and IDs. Use template IDs with estimator_create_from_template to create new estimates.",
26
+ inputSchemaZod: listTemplatesSchema,
27
+ execute: async (args) => {
28
+ if (!client.hasApiKey())
29
+ return client.noApiKeyMessage();
30
+ const params = new URLSearchParams();
31
+ if (args.type)
32
+ params.set("type", args.type);
33
+ const q = params.toString();
34
+ const path = q ? `/estimator/templates?${q}` : "/estimator/templates";
35
+ const data = await client.get(path);
36
+ return JSON.stringify(data);
37
+ },
38
+ };
39
+ // --- estimator_delete_template ---
40
+ const deleteTemplateSchema = z.object({
41
+ id: z.string(),
42
+ });
43
+ export const deleteTemplate = {
44
+ name: "estimator_delete_template",
45
+ description: "Delete a saved estimate template permanently. This does not affect any estimates that were previously created from this template.",
46
+ inputSchemaZod: deleteTemplateSchema,
47
+ execute: async (args) => {
48
+ if (!client.hasApiKey())
49
+ return client.noApiKeyMessage();
50
+ const data = await client.del(`/estimator/templates/${args.id}`);
51
+ return JSON.stringify(data);
52
+ },
53
+ };
54
+ // --- estimator_create_from_template ---
55
+ const createFromTemplateSchema = z.object({
56
+ templateId: z.string(),
57
+ projectName: z.string(),
58
+ clientId: z.string().optional(),
59
+ });
60
+ export const createFromTemplate = {
61
+ name: "estimator_create_from_template",
62
+ description: "Create a new estimate from a saved template. The new estimate gets all the template's line items and structure. Requires: templateId (from estimator_list_templates), projectName. Optionally link to a clientId. The new estimate is created as a draft.",
63
+ inputSchemaZod: createFromTemplateSchema,
64
+ execute: async (args) => {
65
+ if (!client.hasApiKey())
66
+ return client.noApiKeyMessage();
67
+ const data = await client.post("/estimator/estimates", args);
68
+ return JSON.stringify(data);
69
+ },
70
+ };
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ declare const listVersionsSchema: z.ZodObject<{
3
+ estimateId: z.ZodString;
4
+ }, "strip", z.ZodTypeAny, {
5
+ estimateId: string;
6
+ }, {
7
+ estimateId: string;
8
+ }>;
9
+ export declare const listVersions: {
10
+ name: string;
11
+ description: string;
12
+ inputSchemaZod: z.ZodObject<{
13
+ estimateId: z.ZodString;
14
+ }, "strip", z.ZodTypeAny, {
15
+ estimateId: string;
16
+ }, {
17
+ estimateId: string;
18
+ }>;
19
+ execute: (args: z.infer<typeof listVersionsSchema>) => Promise<string>;
20
+ };
21
+ declare const getVersionSchema: z.ZodObject<{
22
+ estimateId: z.ZodString;
23
+ versionId: z.ZodString;
24
+ }, "strip", z.ZodTypeAny, {
25
+ estimateId: string;
26
+ versionId: string;
27
+ }, {
28
+ estimateId: string;
29
+ versionId: string;
30
+ }>;
31
+ export declare const getVersion: {
32
+ name: string;
33
+ description: string;
34
+ inputSchemaZod: z.ZodObject<{
35
+ estimateId: z.ZodString;
36
+ versionId: z.ZodString;
37
+ }, "strip", z.ZodTypeAny, {
38
+ estimateId: string;
39
+ versionId: string;
40
+ }, {
41
+ estimateId: string;
42
+ versionId: string;
43
+ }>;
44
+ execute: (args: z.infer<typeof getVersionSchema>) => Promise<string>;
45
+ };
46
+ export {};
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import * as client from "../lib/kynver-client.js";
3
+ // --- estimator_list_versions ---
4
+ const listVersionsSchema = z.object({
5
+ estimateId: z.string(),
6
+ });
7
+ export const listVersions = {
8
+ name: "estimator_list_versions",
9
+ description: "List all saved version snapshots of an estimate. Versions are created automatically when significant changes are made. Shows version numbers and timestamps. Use this to track the history of changes to an estimate.",
10
+ inputSchemaZod: listVersionsSchema,
11
+ execute: async (args) => {
12
+ if (!client.hasApiKey())
13
+ return client.noApiKeyMessage();
14
+ const data = await client.get(`/estimator/estimates/${args.estimateId}/versions`);
15
+ return JSON.stringify(data);
16
+ },
17
+ };
18
+ // --- estimator_get_version ---
19
+ const getVersionSchema = z.object({
20
+ estimateId: z.string(),
21
+ versionId: z.string(),
22
+ });
23
+ export const getVersion = {
24
+ name: "estimator_get_version",
25
+ description: "Get a specific historical version of an estimate. Returns the full estimate snapshot as it existed at that point in time — all line items, totals, and metadata. Useful for comparing current vs previous versions or reverting changes.",
26
+ inputSchemaZod: getVersionSchema,
27
+ execute: async (args) => {
28
+ if (!client.hasApiKey())
29
+ return client.noApiKeyMessage();
30
+ const data = await client.get(`/estimator/estimates/${args.estimateId}/versions/${args.versionId}`);
31
+ return JSON.stringify(data);
32
+ },
33
+ };
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ declare const analyzePhotosSchema: z.ZodObject<{
3
+ images: z.ZodArray<z.ZodString, "many">;
4
+ prompt: z.ZodOptional<z.ZodString>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ images: string[];
7
+ prompt?: string | undefined;
8
+ }, {
9
+ images: string[];
10
+ prompt?: string | undefined;
11
+ }>;
12
+ export declare const analyzePhotos: {
13
+ name: string;
14
+ description: string;
15
+ inputSchemaZod: z.ZodObject<{
16
+ images: z.ZodArray<z.ZodString, "many">;
17
+ prompt: z.ZodOptional<z.ZodString>;
18
+ }, "strip", z.ZodTypeAny, {
19
+ images: string[];
20
+ prompt?: string | undefined;
21
+ }, {
22
+ images: string[];
23
+ prompt?: string | undefined;
24
+ }>;
25
+ execute: (args: z.infer<typeof analyzePhotosSchema>) => Promise<string>;
26
+ };
27
+ declare const analyzeDocumentSchema: z.ZodObject<{
28
+ text: z.ZodString;
29
+ prompt: z.ZodOptional<z.ZodString>;
30
+ }, "strip", z.ZodTypeAny, {
31
+ text: string;
32
+ prompt?: string | undefined;
33
+ }, {
34
+ text: string;
35
+ prompt?: string | undefined;
36
+ }>;
37
+ export declare const analyzeDocument: {
38
+ name: string;
39
+ description: string;
40
+ inputSchemaZod: z.ZodObject<{
41
+ text: z.ZodString;
42
+ prompt: z.ZodOptional<z.ZodString>;
43
+ }, "strip", z.ZodTypeAny, {
44
+ text: string;
45
+ prompt?: string | undefined;
46
+ }, {
47
+ text: string;
48
+ prompt?: string | undefined;
49
+ }>;
50
+ execute: (args: z.infer<typeof analyzeDocumentSchema>) => Promise<string>;
51
+ };
52
+ export {};
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ import * as client from "../lib/kynver-client.js";
3
+ // --- estimator_analyze_photos ---
4
+ const analyzePhotosSchema = z.object({
5
+ images: z.array(z.string()),
6
+ prompt: z.string().optional(),
7
+ });
8
+ export const analyzePhotos = {
9
+ name: "estimator_analyze_photos",
10
+ description: "Analyze roof photos using Claude's vision AI. Provide image URLs (can be multiple). The AI identifies: roofing materials, visible damage (missing shingles, cracks, moss, ponding), approximate measurements, and repair recommendations. Use the optional prompt parameter to ask specific questions about the photos. Results can inform estimate creation.",
11
+ inputSchemaZod: analyzePhotosSchema,
12
+ execute: async (args) => {
13
+ if (!client.hasApiKey())
14
+ return client.noApiKeyMessage();
15
+ const data = await client.post("/estimator/analyze", {
16
+ type: "photos",
17
+ images: args.images,
18
+ prompt: args.prompt,
19
+ });
20
+ return JSON.stringify(data);
21
+ },
22
+ };
23
+ // --- estimator_analyze_document ---
24
+ const analyzeDocumentSchema = z.object({
25
+ text: z.string(),
26
+ prompt: z.string().optional(),
27
+ });
28
+ export const analyzeDocument = {
29
+ name: "estimator_analyze_document",
30
+ description: "Analyze a text document (inspection report, insurance scope of work, contractor notes) and extract structured estimating data — identified services, quantities, materials, and damage descriptions. Pass the document text in the text field. Use the optional prompt parameter to focus the analysis. Results can be used to create estimates with accurate line items.",
31
+ inputSchemaZod: analyzeDocumentSchema,
32
+ execute: async (args) => {
33
+ if (!client.hasApiKey())
34
+ return client.noApiKeyMessage();
35
+ const data = await client.post("/estimator/analyze", {
36
+ type: "document",
37
+ text: args.text,
38
+ prompt: args.prompt,
39
+ });
40
+ return JSON.stringify(data);
41
+ },
42
+ };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@kynver-app/mcp-estimator",
3
+ "version": "0.1.1",
4
+ "description": "Kynver Estimator MCP server — chat, estimate, and catalog tools",
5
+ "type": "module",
6
+ "bin": {
7
+ "kynver-mcp-estimator": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "prebuild": "node ../../scripts/sync-mcp-sse-server.mjs",
11
+ "build": "node --max-old-space-size=8192 ../../node_modules/typescript/bin/tsc",
12
+ "dev": "node --max-old-space-size=8192 ../../node_modules/typescript/bin/tsc --watch",
13
+ "start": "node dist/index.js",
14
+ "start:sse": "node dist/sse.js"
15
+ },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.27.0",
21
+ "zod": "^3.23.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22",
25
+ "typescript": "^5.0.0"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Kynver Estimator MCP server — stdio entry point.
4
+ * Run: node dist/index.js
5
+ * Env: KYNVER_API_URL (default https://kynver.com), KYNVER_API_KEY (required for all tools)
6
+ */
7
+
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { createEstimatorServer } from "./server.js";
10
+
11
+ const server = createEstimatorServer();
12
+ const transport = new StdioServerTransport();
13
+ await server.connect(transport);
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Thin fetch wrapper for Kynver API. Base URL from KYNVER_API_URL; optional Bearer from KYNVER_API_KEY.
3
+ *
4
+ * Supports per-request user identity via AsyncLocalStorage. When a userId is
5
+ * set in the request context (e.g. from the SSE route), all API calls include
6
+ * X-Kynver-User-Id and X-Kynver-Service-Secret headers so the Kynver API
7
+ * executes operations on behalf of the correct user.
8
+ */
9
+
10
+ import { AsyncLocalStorage } from "node:async_hooks";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Request context — allows the SSE route to set userId per-connection
14
+ // ---------------------------------------------------------------------------
15
+
16
+ interface RequestContext {
17
+ userId?: string;
18
+ }
19
+
20
+ export const requestContext = new AsyncLocalStorage<RequestContext>();
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Config
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const baseUrl = process.env.KYNVER_API_URL ?? "https://kynver.com";
27
+ const apiKey = process.env.KYNVER_API_KEY ?? "";
28
+
29
+ function assertSafeApiPath(path: string): void {
30
+ const p = path.startsWith("/") ? path : `/${path}`;
31
+ if (p.includes("..") || p.includes("\0")) {
32
+ throw new Error("Invalid API path: path traversal is not allowed");
33
+ }
34
+ }
35
+
36
+ function url(path: string): string {
37
+ assertSafeApiPath(path);
38
+ const p = path.startsWith("/") ? path : `/${path}`;
39
+ const apiPath = p.startsWith("/api") ? p : `/api${p}`;
40
+ return `${baseUrl.replace(/\/$/, "")}${apiPath}`;
41
+ }
42
+
43
+ function getHeaders(): Record<string, string> {
44
+ const headers: Record<string, string> = {
45
+ "Content-Type": "application/json",
46
+ };
47
+ if (apiKey) {
48
+ headers["Authorization"] = `Bearer ${apiKey}`;
49
+ }
50
+
51
+ const ctx = requestContext.getStore();
52
+ const userId = ctx?.userId ?? process.env.KYNVER_USER_ID?.trim();
53
+ const serviceSecret = process.env.KYNVER_SERVICE_SECRET?.trim();
54
+
55
+ if (userId) {
56
+ if (!serviceSecret) {
57
+ throw new Error(
58
+ "X-Kynver-User-Id was set in context but KYNVER_SERVICE_SECRET is missing; refusing to send an unscoped request.",
59
+ );
60
+ }
61
+ headers["X-Kynver-User-Id"] = userId;
62
+ headers["X-Kynver-Service-Secret"] = serviceSecret;
63
+ }
64
+
65
+ return headers;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Response handling
70
+ // ---------------------------------------------------------------------------
71
+
72
+ async function handleResponse<T>(res: Response): Promise<T> {
73
+ const text = await res.text();
74
+ if (!res.ok) {
75
+ let msg = text;
76
+ try {
77
+ const j = JSON.parse(text) as { error?: string; message?: string };
78
+ msg = j.error ?? j.message ?? text;
79
+ } catch {
80
+ // use text as-is
81
+ }
82
+ throw new Error(msg || `HTTP ${res.status}`);
83
+ }
84
+ if (!text) return undefined as T;
85
+ try {
86
+ return JSON.parse(text) as T;
87
+ } catch {
88
+ return text as unknown as T;
89
+ }
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // HTTP methods
94
+ // ---------------------------------------------------------------------------
95
+
96
+ export async function get<T = unknown>(path: string): Promise<T> {
97
+ const res = await fetch(url(path), {
98
+ method: "GET",
99
+ headers: getHeaders(),
100
+ });
101
+ return handleResponse<T>(res);
102
+ }
103
+
104
+ export async function post<T = unknown>(path: string, body: unknown): Promise<T> {
105
+ const res = await fetch(url(path), {
106
+ method: "POST",
107
+ headers: getHeaders(),
108
+ body: JSON.stringify(body),
109
+ });
110
+ return handleResponse<T>(res);
111
+ }
112
+
113
+ export async function patch<T = unknown>(path: string, body: unknown): Promise<T> {
114
+ const res = await fetch(url(path), {
115
+ method: "PATCH",
116
+ headers: getHeaders(),
117
+ body: JSON.stringify(body),
118
+ });
119
+ return handleResponse<T>(res);
120
+ }
121
+
122
+ export async function del<T = unknown>(path: string): Promise<T> {
123
+ const res = await fetch(url(path), {
124
+ method: "DELETE",
125
+ headers: getHeaders(),
126
+ });
127
+ return handleResponse<T>(res);
128
+ }
129
+
130
+ export function hasApiKey(): boolean {
131
+ return !!apiKey;
132
+ }
133
+
134
+ export function noApiKeyMessage(): string {
135
+ return "No API key configured. Set KYNVER_API_KEY in your MCP server config.";
136
+ }