@tenonhq/dovetail-mcp 0.0.14 → 0.0.16

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/registry.js CHANGED
@@ -191,7 +191,7 @@ function buildDescriptors(deps) {
191
191
  },
192
192
  {
193
193
  name: "servicenow_query_table",
194
- description: "Read-only GET against the ServiceNow Table API. Required: table (lower-case identifier), sysparm_query (ServiceNow encoded query). Optional: fields[] limits the columns returned, limit caps row count (default 100, max 1000). Tables on the deny list (sys_user_password, sys_credential, etc.) are rejected unless SINC_MCP_SN_TABLE_OVERRIDE=<table> is set.",
194
+ description: "Read-only GET against the ServiceNow Table API. Required: table (lower-case identifier), sysparm_query (ServiceNow encoded query). Optional: fields[] limits the columns returned, limit caps row count (default 100, max 1000), env retargets a single call to a different instance by naming an env file in the server's working directory ('prod' or '.env.prod' → reads creds from .env.prod; omit to use the startup instance). Tables on the deny list (sys_user_password, sys_credential, etc.) are rejected unless SINC_MCP_SN_TABLE_OVERRIDE=<table> is set.",
195
195
  shape: servicenow_1.servicenowQueryTableSchema.shape,
196
196
  annotations: dovetail_mcp_kit_1.READ_ONLY,
197
197
  handler: function (args) {
@@ -4,15 +4,18 @@ export declare var servicenowQueryTableSchema: z.ZodObject<{
4
4
  sysparm_query: z.ZodString;
5
5
  fields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
6
6
  limit: z.ZodOptional<z.ZodNumber>;
7
+ env: z.ZodOptional<z.ZodString>;
7
8
  }, "strict", z.ZodTypeAny, {
8
9
  table: string;
9
10
  sysparm_query: string;
10
11
  fields?: string[] | undefined;
11
12
  limit?: number | undefined;
13
+ env?: string | undefined;
12
14
  }, {
13
15
  table: string;
14
16
  sysparm_query: string;
15
17
  fields?: string[] | undefined;
16
18
  limit?: number | undefined;
19
+ env?: string | undefined;
17
20
  }>;
18
21
  export type ServicenowQueryTableInput = z.infer<typeof servicenowQueryTableSchema>;
@@ -6,5 +6,17 @@ exports.servicenowQueryTableSchema = zod_1.z.object({
6
6
  table: zod_1.z.string().regex(/^[a-z][a-z0-9_]*$/, "Table name must match /^[a-z][a-z0-9_]*$/"),
7
7
  sysparm_query: zod_1.z.string(),
8
8
  fields: zod_1.z.array(zod_1.z.string().min(1)).optional(),
9
- limit: zod_1.z.number().int().min(1).max(1000).optional()
9
+ limit: zod_1.z.number().int().min(1).max(1000).optional(),
10
+ // Optional per-call instance retarget. Names an env file in the server's
11
+ // working directory: a bare token like "prod" or "workshop" resolves to
12
+ // ".env.<token>", or pass the full ".env.<name>" basename. Path separators
13
+ // and traversal are rejected (no arbitrary filesystem reads) — see
14
+ // resolveEnvFilePath in tools/servicenow.ts. Omit to use the instance the
15
+ // server was started with.
16
+ env: zod_1.z
17
+ .string()
18
+ .min(1)
19
+ .max(100)
20
+ .regex(/^(\.env(\.[A-Za-z0-9_-]+)?|[A-Za-z0-9_-]+)$/, "env must be an env-file name like 'prod' or '.env.prod' — no path separators or '..'")
21
+ .optional()
10
22
  }).strict();
@@ -14,7 +14,17 @@ import { ServicenowQueryTableInput } from "../schemas/servicenow";
14
14
  export interface ServiceNowDeps {
15
15
  safety: ServiceNowSafetyConfig;
16
16
  clientFactory?: () => ServiceNowClient;
17
+ /** Injectable for tests — defaults to the real createClientFromEnvFile. */
18
+ clientFromEnvFile?: (envPath: string) => ServiceNowClient;
17
19
  }
20
+ /**
21
+ * Resolve the caller-supplied `env` token to an absolute env-file path inside
22
+ * the server's working directory. The token is untrusted tool input, so this
23
+ * is defense-in-depth on top of the schema regex: reject anything with a path
24
+ * separator or traversal, accept only a `.env` basename (or a bare token that
25
+ * maps to `.env.<token>`), and confirm the resolved path stays within cwd.
26
+ */
27
+ export declare function resolveEnvFilePath(env: string): string;
18
28
  export declare function servicenowQueryTable(args: ServicenowQueryTableInput, deps: ServiceNowDeps): Promise<{
19
29
  table: string;
20
30
  count: number;
@@ -9,10 +9,43 @@
9
9
  * Tables on the deny list (sys_user_password, sys_credential, sys_audit, …)
10
10
  * are rejected unless the operator opts in via SINC_MCP_SN_TABLE_OVERRIDE.
11
11
  */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
12
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.resolveEnvFilePath = resolveEnvFilePath;
13
17
  exports.servicenowQueryTable = servicenowQueryTable;
18
+ const path_1 = __importDefault(require("path"));
14
19
  const dovetail_servicenow_1 = require("@tenonhq/dovetail-servicenow");
15
- function resolveClient(deps) {
20
+ /**
21
+ * Resolve the caller-supplied `env` token to an absolute env-file path inside
22
+ * the server's working directory. The token is untrusted tool input, so this
23
+ * is defense-in-depth on top of the schema regex: reject anything with a path
24
+ * separator or traversal, accept only a `.env` basename (or a bare token that
25
+ * maps to `.env.<token>`), and confirm the resolved path stays within cwd.
26
+ */
27
+ function resolveEnvFilePath(env) {
28
+ if (typeof env !== "string" || env.length === 0) {
29
+ throw new Error("env must be a non-empty string.");
30
+ }
31
+ if (/[\\/]/.test(env) || env.indexOf("..") !== -1) {
32
+ throw new Error("env '" + env + "' is invalid — no path separators or '..'; " +
33
+ "pass a name like 'prod' or '.env.prod'.");
34
+ }
35
+ var basename = env.indexOf(".env") === 0 ? env : ".env." + env;
36
+ var cwd = process.cwd();
37
+ var resolved = path_1.default.resolve(cwd, basename);
38
+ // path.resolve collapses any residual traversal; verify containment.
39
+ if (resolved !== path_1.default.join(cwd, basename) || path_1.default.dirname(resolved) !== cwd) {
40
+ throw new Error("env '" + env + "' resolves outside the working directory.");
41
+ }
42
+ return resolved;
43
+ }
44
+ function resolveClient(deps, env) {
45
+ if (env) {
46
+ var fromEnvFile = deps.clientFromEnvFile || dovetail_servicenow_1.createClientFromEnvFile;
47
+ return fromEnvFile(resolveEnvFilePath(env));
48
+ }
16
49
  if (deps.clientFactory) {
17
50
  return deps.clientFactory();
18
51
  }
@@ -29,7 +62,7 @@ async function servicenowQueryTable(args, deps) {
29
62
  throw new Error("Table '" + args.table + "' is in the default deny list. " +
30
63
  "Set SINC_MCP_SN_TABLE_OVERRIDE=" + args.table + " (comma-separated for multiple) to enable.");
31
64
  }
32
- var client = resolveClient(deps);
65
+ var client = resolveClient(deps, args.env);
33
66
  var limit = args.limit !== undefined ? args.limit : 100;
34
67
  var records;
35
68
  if (args.fields && args.fields.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-mcp",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "MCP server exposing read tools for ClickUp / Gmail / Calendar / ServiceNow plus gated ClickUp writes, backed by the Dovetail integration packages.",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "@tenonhq/dovetail-google-auth": "~0.0.12",
23
23
  "@tenonhq/dovetail-google-calendar": "~0.0.11",
24
24
  "@tenonhq/dovetail-mcp-kit": "~0.0.1",
25
- "@tenonhq/dovetail-servicenow": "~0.0.19",
25
+ "@tenonhq/dovetail-servicenow": "~0.0.21",
26
26
  "dotenv": "^16.3.1",
27
27
  "zod": "^3.23.0"
28
28
  },