@nimbus-dev/connectors 0.2.0 → 0.2.2
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/README.md +18 -4
- package/connectors/airflow/src/search-filter.ts +2 -15
- package/connectors/airflow/src/server.ts +2 -87
- package/connectors/airflow/src/tools.ts +74 -0
- package/connectors/apple/src/adapters.ts +326 -0
- package/connectors/apple/src/apple-mail-core.ts +4 -8
- package/connectors/apple/src/calendar-tools.ts +28 -8
- package/connectors/apple/src/server.ts +23 -512
- package/connectors/apple/src/tools.ts +18 -44
- package/connectors/argocd/src/server.ts +25 -38
- package/connectors/athena/src/tools.ts +15 -34
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +197 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +133 -0
- package/connectors/bigeye/src/search-filter.ts +4 -15
- package/connectors/bigeye/src/server.ts +38 -38
- package/connectors/bigquery/src/tools.ts +1 -26
- package/connectors/bitbucket/src/server.ts +2 -244
- package/connectors/bitbucket/src/tools.ts +241 -0
- package/connectors/bitrise/src/server.ts +2 -79
- package/connectors/bitrise/src/tools.ts +76 -0
- package/connectors/canva/src/search-filter.ts +2 -11
- package/connectors/canva/src/server.ts +2 -73
- package/connectors/canva/src/tools.ts +66 -0
- package/connectors/circleci/src/server.ts +2 -176
- package/connectors/circleci/src/tools.ts +193 -0
- package/connectors/cloud-logging/src/tools.ts +14 -27
- package/connectors/cloudwatch/src/tools.ts +15 -34
- package/connectors/codemagic/src/server.ts +2 -70
- package/connectors/codemagic/src/tools.ts +71 -0
- package/connectors/confluence/src/server.ts +2 -301
- package/connectors/confluence/src/tools.ts +320 -0
- package/connectors/dagster/src/server.ts +2 -214
- package/connectors/dagster/src/tools.ts +187 -0
- package/connectors/databricks/src/search-filter.ts +7 -7
- package/connectors/databricks/src/server.ts +2 -77
- package/connectors/databricks/src/tools.ts +66 -0
- package/connectors/datadog/src/server.ts +2 -48
- package/connectors/datadog/src/tools.ts +48 -0
- package/connectors/dataprofile/src/profile.ts +16 -46
- package/connectors/dbt/src/server.ts +2 -92
- package/connectors/dbt/src/tools.ts +80 -0
- package/connectors/dependencytrack/src/server.ts +2 -76
- package/connectors/dependencytrack/src/tools.ts +65 -0
- package/connectors/discord/src/server.ts +2 -85
- package/connectors/discord/src/tools.ts +82 -0
- package/connectors/elasticsearch/src/tools.ts +7 -37
- package/connectors/fastmail/src/jmap-client.ts +202 -0
- package/connectors/fastmail/src/jmap-core.ts +8 -21
- package/connectors/fastmail/src/server.ts +2 -187
- package/connectors/fastmail/src/tools.ts +4 -23
- package/connectors/figma/src/search-filter.ts +2 -11
- package/connectors/figma/src/server.ts +2 -113
- package/connectors/figma/src/tools.ts +99 -0
- package/connectors/firebase/src/server.ts +2 -90
- package/connectors/firebase/src/token.ts +2 -5
- package/connectors/firebase/src/tools.ts +106 -0
- package/connectors/flagsmith/src/server.ts +2 -98
- package/connectors/flagsmith/src/tools.ts +95 -0
- package/connectors/flux/src/server.ts +52 -53
- package/connectors/gcp/src/server.ts +2 -132
- package/connectors/gcp/src/tools.ts +123 -0
- package/connectors/github/src/server.ts +2 -328
- package/connectors/github/src/tools.ts +311 -0
- package/connectors/github-actions/src/server.ts +2 -205
- package/connectors/github-actions/src/tools.ts +209 -0
- package/connectors/gitlab/src/server.ts +2 -384
- package/connectors/gitlab/src/tools.ts +392 -0
- package/connectors/gmail/src/server.ts +2 -270
- package/connectors/gmail/src/tools.ts +209 -0
- package/connectors/google-drive/src/server.ts +2 -524
- package/connectors/google-drive/src/tools.ts +529 -0
- package/connectors/google-meet/src/server.ts +2 -82
- package/connectors/google-meet/src/tools.ts +87 -0
- package/connectors/google-photos/src/server.ts +2 -132
- package/connectors/google-photos/src/tools.ts +138 -0
- package/connectors/grafana/src/server.ts +2 -53
- package/connectors/grafana/src/tools.ts +53 -0
- package/connectors/great-expectations/src/gx-parse.ts +14 -45
- package/connectors/greenhouse/src/server.ts +2 -63
- package/connectors/greenhouse/src/tools.ts +68 -0
- package/connectors/hubspot/src/server.ts +2 -77
- package/connectors/hubspot/src/tools.ts +70 -0
- package/connectors/iac/src/server.ts +2 -137
- package/connectors/iac/src/tools.ts +152 -0
- package/connectors/imap/src/imap-core.ts +29 -82
- package/connectors/imap/src/server.ts +33 -220
- package/connectors/intercom/src/server.ts +2 -61
- package/connectors/intercom/src/tools.ts +58 -0
- package/connectors/jenkins/src/jenkins-api.ts +2 -6
- package/connectors/jenkins/src/server.ts +2 -215
- package/connectors/jenkins/src/tools.ts +240 -0
- package/connectors/jira/src/server.ts +2 -301
- package/connectors/jira/src/tools.ts +325 -0
- package/connectors/kubernetes/src/server.ts +2 -155
- package/connectors/kubernetes/src/tools.ts +159 -0
- package/connectors/launchdarkly/src/server.ts +2 -75
- package/connectors/launchdarkly/src/tools.ts +72 -0
- package/connectors/lever/src/search-filter.ts +4 -12
- package/connectors/lever/src/server.ts +2 -60
- package/connectors/lever/src/tools.ts +61 -0
- package/connectors/linear/src/server.ts +2 -459
- package/connectors/linear/src/tools.ts +482 -0
- package/connectors/localdb/src/sql-scan.ts +13 -41
- package/connectors/localdb/src/tools.ts +1 -1
- package/connectors/looker/src/search-filter.ts +2 -15
- package/connectors/looker/src/server.ts +35 -70
- package/connectors/mendeley/src/server.ts +2 -55
- package/connectors/mendeley/src/tools.ts +54 -0
- package/connectors/mercury/src/server.ts +2 -57
- package/connectors/mercury/src/tools.ts +50 -0
- package/connectors/metabase/src/search-filter.ts +4 -15
- package/connectors/metabase/src/server.ts +2 -79
- package/connectors/metabase/src/tools.ts +64 -0
- package/connectors/miro/src/server.ts +2 -72
- package/connectors/miro/src/tools.ts +65 -0
- package/connectors/mlflow/src/search-filter.ts +3 -7
- package/connectors/mlflow/src/server.ts +20 -40
- package/connectors/monte-carlo/src/server.ts +50 -57
- package/connectors/netlify/src/search-filter.ts +6 -14
- package/connectors/netlify/src/server.ts +2 -60
- package/connectors/netlify/src/tools.ts +53 -0
- package/connectors/newrelic/src/server.ts +2 -40
- package/connectors/newrelic/src/tools.ts +37 -0
- package/connectors/notion/src/server.ts +2 -335
- package/connectors/notion/src/tools.ts +344 -0
- package/connectors/obsidian/src/server.ts +2 -425
- package/connectors/obsidian/src/tools.ts +466 -0
- package/connectors/onedrive/src/server.ts +2 -249
- package/connectors/onedrive/src/tools.ts +253 -0
- package/connectors/outlook/src/server.ts +2 -351
- package/connectors/outlook/src/tools.ts +357 -0
- package/connectors/pagerduty/src/server.ts +2 -132
- package/connectors/pagerduty/src/tools.ts +126 -0
- package/connectors/pipedrive/src/server.ts +2 -58
- package/connectors/pipedrive/src/tools.ts +63 -0
- package/connectors/powerbi/src/search-filter.ts +2 -15
- package/connectors/powerbi/src/server.ts +43 -68
- package/connectors/prefect/src/search-filter.ts +3 -16
- package/connectors/prefect/src/server.ts +2 -90
- package/connectors/prefect/src/tools.ts +83 -0
- package/connectors/protonmail/src/mail-core.ts +16 -75
- package/connectors/protonmail/src/server.ts +34 -214
- package/connectors/raindrop/src/server.ts +2 -103
- package/connectors/raindrop/src/tools.ts +103 -0
- package/connectors/ramp/src/server.ts +2 -105
- package/connectors/ramp/src/tools.ts +98 -0
- package/connectors/readwise/src/server.ts +2 -88
- package/connectors/readwise/src/tools.ts +81 -0
- package/connectors/sagemaker/src/tools.ts +19 -38
- package/connectors/salesforce/src/search-filter.ts +4 -11
- package/connectors/salesforce/src/server.ts +2 -90
- package/connectors/salesforce/src/tools.ts +75 -0
- package/connectors/semgrep/src/search-filter.ts +3 -10
- package/connectors/semgrep/src/server.ts +2 -104
- package/connectors/semgrep/src/tools.ts +92 -0
- package/connectors/sentry/src/server.ts +2 -58
- package/connectors/sentry/src/tools.ts +53 -0
- package/connectors/slack/src/server.ts +2 -352
- package/connectors/slack/src/tools.ts +376 -0
- package/connectors/snowflake/src/search-filter.ts +4 -19
- package/connectors/snowflake/src/server.ts +6 -25
- package/connectors/snyk/src/server.ts +2 -140
- package/connectors/snyk/src/tools.ts +136 -0
- package/connectors/sonarqube/src/server.ts +2 -111
- package/connectors/sonarqube/src/tools.ts +98 -0
- package/connectors/stackoverflow/src/server.ts +2 -77
- package/connectors/stackoverflow/src/tools.ts +66 -0
- package/connectors/storybook/src/storybook-parse.ts +3 -6
- package/connectors/stripe/src/server.ts +2 -61
- package/connectors/stripe/src/tools.ts +54 -0
- package/connectors/superset/src/search-filter.ts +4 -15
- package/connectors/superset/src/server.ts +2 -115
- package/connectors/superset/src/tools.ts +98 -0
- package/connectors/tableau/src/search-filter.ts +2 -15
- package/connectors/tableau/src/server.ts +13 -65
- package/connectors/teams/src/server.ts +2 -249
- package/connectors/teams/src/tools.ts +283 -0
- package/connectors/testflight/src/server.ts +2 -70
- package/connectors/testflight/src/tools.ts +79 -0
- package/connectors/vercel/src/search-filter.ts +2 -10
- package/connectors/vercel/src/server.ts +2 -75
- package/connectors/vercel/src/tools.ts +73 -0
- package/connectors/vertex-ai/src/tools.ts +23 -33
- package/connectors/wiz/src/search-filter.ts +6 -30
- package/connectors/wiz/src/server.ts +2 -188
- package/connectors/wiz/src/tools.ts +168 -0
- package/connectors/zendesk/src/server.ts +2 -73
- package/connectors/zendesk/src/tools.ts +60 -0
- package/connectors/zoom/src/search-filter.ts +1 -7
- package/connectors/zoom/src/server.ts +2 -94
- package/connectors/zoom/src/tools.ts +93 -0
- package/connectors/zotero/src/server.ts +2 -78
- package/connectors/zotero/src/tools.ts +67 -0
- package/package.json +15 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +13 -6
- package/shared/cli-json-kit.ts +150 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/cursor-list-tool.ts +29 -0
- package/shared/env-json-api.ts +209 -0
- package/shared/fetch-json-text.ts +27 -0
- package/shared/github-rest.ts +62 -0
- package/shared/graphql-json.ts +60 -0
- package/shared/imap-mail-core.ts +91 -0
- package/shared/imap-tool-kit.ts +57 -23
- package/shared/imapflow-adapter.ts +417 -0
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/standalone/src/launcher.ts +19 -7
|
@@ -1,139 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
fetchBearerAuthorizedJson,
|
|
7
|
-
resolveUrlWithBase,
|
|
8
|
-
} from "../../../shared/fetch-bearer-json.ts";
|
|
9
|
-
import { createRegisterSimpleTool, createZodToolRegistrar } from "../../../shared/mcp-tool-kit.ts";
|
|
10
|
-
import { makeRestToolRegistrar } from "../../../shared/rest-tool-kit.ts";
|
|
11
|
-
|
|
12
|
-
const PHOTOS_BASE = "https://photoslibrary.googleapis.com/v1";
|
|
13
|
-
|
|
14
|
-
async function photosFetch(
|
|
15
|
-
token: string,
|
|
16
|
-
path: string,
|
|
17
|
-
init?: RequestInit,
|
|
18
|
-
): Promise<{ ok: boolean; status: number; json: unknown; text: string }> {
|
|
19
|
-
const url = resolveUrlWithBase(PHOTOS_BASE, path);
|
|
20
|
-
return fetchBearerAuthorizedJson(url, token, init, { "Content-Type": "application/json" });
|
|
21
|
-
}
|
|
3
|
+
import { registerGooglePhotosTools } from "./tools.ts";
|
|
22
4
|
|
|
23
5
|
const server = new McpServer({ name: "nimbus-google-photos", version: "0.1.0" });
|
|
24
6
|
|
|
25
|
-
|
|
26
|
-
const reg = createZodToolRegistrar(registerSimpleTool);
|
|
27
|
-
|
|
28
|
-
/** Standard Photos tool: token → photosFetch(buildPath[, buildInit]) → mcpJsonResultIfOk("Google Photos API"). */
|
|
29
|
-
const registerPhotosTool = makeRestToolRegistrar({
|
|
30
|
-
registrar: reg,
|
|
31
|
-
tokenEnv: "GOOGLE_OAUTH_ACCESS_TOKEN",
|
|
32
|
-
serviceLabel: "Google Photos API",
|
|
33
|
-
fetch: photosFetch,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const gphotosAlbumListArgs = z.object({
|
|
37
|
-
pageSize: z.number().int().min(1).max(50).optional(),
|
|
38
|
-
pageToken: z.string().optional(),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
registerPhotosTool(
|
|
42
|
-
"gphotos_album_list",
|
|
43
|
-
"List Google Photos albums (metadata). Pagination via pageToken.",
|
|
44
|
-
gphotosAlbumListArgs,
|
|
45
|
-
(parsed) => {
|
|
46
|
-
const u = new URL(`${PHOTOS_BASE}/albums`);
|
|
47
|
-
u.searchParams.set("pageSize", String(parsed.pageSize ?? 25));
|
|
48
|
-
if (parsed.pageToken !== undefined && parsed.pageToken !== "") {
|
|
49
|
-
u.searchParams.set("pageToken", parsed.pageToken);
|
|
50
|
-
}
|
|
51
|
-
return `${u.pathname}${u.search}`;
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const gphotosAlbumGetArgs = z.object({
|
|
56
|
-
albumId: z.string().min(1),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
registerPhotosTool(
|
|
60
|
-
"gphotos_album_get",
|
|
61
|
-
"Get a single album by id (title, mediaItemsCount, coverPhotoBaseUrl).",
|
|
62
|
-
gphotosAlbumGetArgs,
|
|
63
|
-
(parsed) => `/albums/${encodeURIComponent(parsed.albumId)}`,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const gphotosMediaListArgs = z.object({
|
|
67
|
-
albumId: z.string().min(1).optional(),
|
|
68
|
-
pageSize: z.number().int().min(1).max(100).optional(),
|
|
69
|
-
pageToken: z.string().optional(),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
registerPhotosTool(
|
|
73
|
-
"gphotos_media_list",
|
|
74
|
-
"List media items (metadata + baseUrl/productUrl only). Optional albumId scopes to one album.",
|
|
75
|
-
gphotosMediaListArgs,
|
|
76
|
-
() => "/mediaItems:search",
|
|
77
|
-
(parsed) => {
|
|
78
|
-
const body: Record<string, unknown> = {
|
|
79
|
-
pageSize: parsed.pageSize ?? 50,
|
|
80
|
-
};
|
|
81
|
-
if (parsed.pageToken !== undefined && parsed.pageToken !== "") {
|
|
82
|
-
body["pageToken"] = parsed.pageToken;
|
|
83
|
-
}
|
|
84
|
-
if (parsed.albumId !== undefined && parsed.albumId !== "") {
|
|
85
|
-
body["albumId"] = parsed.albumId;
|
|
86
|
-
}
|
|
87
|
-
return { method: "POST", body: JSON.stringify(body) };
|
|
88
|
-
},
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const gphotosMediaGetArgs = z.object({
|
|
92
|
-
mediaItemId: z.string().min(1),
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
registerPhotosTool(
|
|
96
|
-
"gphotos_media_get",
|
|
97
|
-
"Get a single media item metadata by id.",
|
|
98
|
-
gphotosMediaGetArgs,
|
|
99
|
-
(parsed) => `/mediaItems/${encodeURIComponent(parsed.mediaItemId)}`,
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const gphotosMediaSearchArgs = z.object({
|
|
103
|
-
albumId: z.string().min(1).optional(),
|
|
104
|
-
pageSize: z.number().int().min(1).max(100).optional(),
|
|
105
|
-
pageToken: z.string().optional(),
|
|
106
|
-
includeArchivedMedia: z.boolean().optional(),
|
|
107
|
-
excludeNonAppCreatedData: z.boolean().optional(),
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
registerPhotosTool(
|
|
111
|
-
"gphotos_media_search",
|
|
112
|
-
"Search media items (metadata only). Optional album filter; supports pagination.",
|
|
113
|
-
gphotosMediaSearchArgs,
|
|
114
|
-
() => "/mediaItems:search",
|
|
115
|
-
(parsed) => {
|
|
116
|
-
const body: Record<string, unknown> = {
|
|
117
|
-
pageSize: parsed.pageSize ?? 50,
|
|
118
|
-
};
|
|
119
|
-
if (parsed.pageToken !== undefined && parsed.pageToken !== "") {
|
|
120
|
-
body["pageToken"] = parsed.pageToken;
|
|
121
|
-
}
|
|
122
|
-
if (parsed.albumId !== undefined && parsed.albumId !== "") {
|
|
123
|
-
body["albumId"] = parsed.albumId;
|
|
124
|
-
}
|
|
125
|
-
const filters: Record<string, unknown> = {};
|
|
126
|
-
if (parsed.includeArchivedMedia === true) {
|
|
127
|
-
filters["includeArchivedMedia"] = true;
|
|
128
|
-
}
|
|
129
|
-
if (parsed.excludeNonAppCreatedData === true) {
|
|
130
|
-
filters["excludeNonAppCreatedData"] = true;
|
|
131
|
-
}
|
|
132
|
-
if (Object.keys(filters).length > 0) {
|
|
133
|
-
body["filters"] = filters;
|
|
134
|
-
}
|
|
135
|
-
return { method: "POST", body: JSON.stringify(body) };
|
|
136
|
-
},
|
|
137
|
-
);
|
|
7
|
+
registerGooglePhotosTools(server);
|
|
138
8
|
|
|
139
9
|
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
fetchBearerAuthorizedJson,
|
|
4
|
+
resolveUrlWithBase,
|
|
5
|
+
} from "../../../shared/fetch-bearer-json.ts";
|
|
6
|
+
import { createRegisterSimpleTool, createZodToolRegistrar } from "../../../shared/mcp-tool-kit.ts";
|
|
7
|
+
import { makeRestToolRegistrar } from "../../../shared/rest-tool-kit.ts";
|
|
8
|
+
|
|
9
|
+
const PHOTOS_BASE = "https://photoslibrary.googleapis.com/v1";
|
|
10
|
+
|
|
11
|
+
async function photosFetch(
|
|
12
|
+
token: string,
|
|
13
|
+
path: string,
|
|
14
|
+
init?: RequestInit,
|
|
15
|
+
): Promise<{ ok: boolean; status: number; json: unknown; text: string }> {
|
|
16
|
+
const url = resolveUrlWithBase(PHOTOS_BASE, path);
|
|
17
|
+
return fetchBearerAuthorizedJson(url, token, init, { "Content-Type": "application/json" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The `mediaItems:search` body both media tools start from: the page size (default 50), then the
|
|
22
|
+
* page token and the album scope, each only when non-empty.
|
|
23
|
+
*/
|
|
24
|
+
function mediaSearchBody(parsed: {
|
|
25
|
+
readonly pageSize?: number | undefined;
|
|
26
|
+
readonly pageToken?: string | undefined;
|
|
27
|
+
readonly albumId?: string | undefined;
|
|
28
|
+
}): Record<string, unknown> {
|
|
29
|
+
const body: Record<string, unknown> = {
|
|
30
|
+
pageSize: parsed.pageSize ?? 50,
|
|
31
|
+
};
|
|
32
|
+
if (parsed.pageToken !== undefined && parsed.pageToken !== "") {
|
|
33
|
+
body["pageToken"] = parsed.pageToken;
|
|
34
|
+
}
|
|
35
|
+
if (parsed.albumId !== undefined && parsed.albumId !== "") {
|
|
36
|
+
body["albumId"] = parsed.albumId;
|
|
37
|
+
}
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registerGooglePhotosTools(server: { tool: (...args: never) => unknown }): void {
|
|
42
|
+
const registerSimpleTool = createRegisterSimpleTool(server);
|
|
43
|
+
const reg = createZodToolRegistrar(registerSimpleTool);
|
|
44
|
+
|
|
45
|
+
/** Standard Photos tool: token → photosFetch(buildPath[, buildInit]) → mcpJsonResultIfOk("Google Photos API"). */
|
|
46
|
+
const registerPhotosTool = makeRestToolRegistrar({
|
|
47
|
+
registrar: reg,
|
|
48
|
+
tokenEnv: "GOOGLE_OAUTH_ACCESS_TOKEN",
|
|
49
|
+
serviceLabel: "Google Photos API",
|
|
50
|
+
fetch: photosFetch,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const gphotosAlbumListArgs = z.object({
|
|
54
|
+
pageSize: z.number().int().min(1).max(50).optional(),
|
|
55
|
+
pageToken: z.string().optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
registerPhotosTool(
|
|
59
|
+
"gphotos_album_list",
|
|
60
|
+
"List Google Photos albums (metadata). Pagination via pageToken.",
|
|
61
|
+
gphotosAlbumListArgs,
|
|
62
|
+
(parsed) => {
|
|
63
|
+
const u = new URL(`${PHOTOS_BASE}/albums`);
|
|
64
|
+
u.searchParams.set("pageSize", String(parsed.pageSize ?? 25));
|
|
65
|
+
if (parsed.pageToken !== undefined && parsed.pageToken !== "") {
|
|
66
|
+
u.searchParams.set("pageToken", parsed.pageToken);
|
|
67
|
+
}
|
|
68
|
+
// Relative to PHOTOS_BASE, like every other tool here: the fetcher prefixes the base, which
|
|
69
|
+
// already ends in /v1, so returning `u.pathname` requested /v1/v1/albums.
|
|
70
|
+
return `/albums${u.search}`;
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const gphotosAlbumGetArgs = z.object({
|
|
75
|
+
albumId: z.string().min(1),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
registerPhotosTool(
|
|
79
|
+
"gphotos_album_get",
|
|
80
|
+
"Get a single album by id (title, mediaItemsCount, coverPhotoBaseUrl).",
|
|
81
|
+
gphotosAlbumGetArgs,
|
|
82
|
+
(parsed) => `/albums/${encodeURIComponent(parsed.albumId)}`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const gphotosMediaListArgs = z.object({
|
|
86
|
+
albumId: z.string().min(1).optional(),
|
|
87
|
+
pageSize: z.number().int().min(1).max(100).optional(),
|
|
88
|
+
pageToken: z.string().optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
registerPhotosTool(
|
|
92
|
+
"gphotos_media_list",
|
|
93
|
+
"List media items (metadata + baseUrl/productUrl only). Optional albumId scopes to one album.",
|
|
94
|
+
gphotosMediaListArgs,
|
|
95
|
+
() => "/mediaItems:search",
|
|
96
|
+
(parsed) => ({ method: "POST", body: JSON.stringify(mediaSearchBody(parsed)) }),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const gphotosMediaGetArgs = z.object({
|
|
100
|
+
mediaItemId: z.string().min(1),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
registerPhotosTool(
|
|
104
|
+
"gphotos_media_get",
|
|
105
|
+
"Get a single media item metadata by id.",
|
|
106
|
+
gphotosMediaGetArgs,
|
|
107
|
+
(parsed) => `/mediaItems/${encodeURIComponent(parsed.mediaItemId)}`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const gphotosMediaSearchArgs = z.object({
|
|
111
|
+
albumId: z.string().min(1).optional(),
|
|
112
|
+
pageSize: z.number().int().min(1).max(100).optional(),
|
|
113
|
+
pageToken: z.string().optional(),
|
|
114
|
+
includeArchivedMedia: z.boolean().optional(),
|
|
115
|
+
excludeNonAppCreatedData: z.boolean().optional(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
registerPhotosTool(
|
|
119
|
+
"gphotos_media_search",
|
|
120
|
+
"Search media items (metadata only). Optional album filter; supports pagination.",
|
|
121
|
+
gphotosMediaSearchArgs,
|
|
122
|
+
() => "/mediaItems:search",
|
|
123
|
+
(parsed) => {
|
|
124
|
+
const body = mediaSearchBody(parsed);
|
|
125
|
+
const filters: Record<string, unknown> = {};
|
|
126
|
+
if (parsed.includeArchivedMedia === true) {
|
|
127
|
+
filters["includeArchivedMedia"] = true;
|
|
128
|
+
}
|
|
129
|
+
if (parsed.excludeNonAppCreatedData === true) {
|
|
130
|
+
filters["excludeNonAppCreatedData"] = true;
|
|
131
|
+
}
|
|
132
|
+
if (Object.keys(filters).length > 0) {
|
|
133
|
+
body["filters"] = filters;
|
|
134
|
+
}
|
|
135
|
+
return { method: "POST", body: JSON.stringify(body) };
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -1,61 +1,10 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
createRegisterSimpleTool,
|
|
7
|
-
createZodToolRegistrar,
|
|
8
|
-
mcpJsonResult as jsonResult,
|
|
9
|
-
} from "../../../shared/mcp-tool-kit.ts";
|
|
10
|
-
|
|
11
|
-
function baseUrl(): string {
|
|
12
|
-
const u = process.env["GRAFANA_URL"]?.trim();
|
|
13
|
-
if (u === undefined || u === "") {
|
|
14
|
-
throw new Error("GRAFANA_URL is not set");
|
|
15
|
-
}
|
|
16
|
-
return u.replace(/\/$/, "");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function authHeaders(): Record<string, string> {
|
|
20
|
-
const tok = process.env["GRAFANA_API_TOKEN"]?.trim();
|
|
21
|
-
if (tok === undefined || tok === "") {
|
|
22
|
-
throw new Error("GRAFANA_API_TOKEN is not set");
|
|
23
|
-
}
|
|
24
|
-
return { Authorization: `Bearer ${tok}`, Accept: "application/json" };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function grafanaGet(path: string): Promise<unknown> {
|
|
28
|
-
const pathPart = path.startsWith("/") ? path : `/${path}`;
|
|
29
|
-
const res = await fetch(`${baseUrl()}${pathPart}`, {
|
|
30
|
-
headers: authHeaders(),
|
|
31
|
-
});
|
|
32
|
-
const text = await res.text();
|
|
33
|
-
if (!res.ok) {
|
|
34
|
-
throw new Error(`Grafana ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
return JSON.parse(text) as unknown;
|
|
38
|
-
} catch {
|
|
39
|
-
return { raw: text };
|
|
40
|
-
}
|
|
41
|
-
}
|
|
3
|
+
import { registerGrafanaTools } from "./tools.ts";
|
|
42
4
|
|
|
43
5
|
const mcp = new McpServer({ name: "nimbus-grafana", version: "0.1.0" });
|
|
44
|
-
const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
|
|
45
|
-
|
|
46
|
-
reg("grafana_alert_list", "List alert rules (Ruler API).", z.object({}), async () =>
|
|
47
|
-
jsonResult(await grafanaGet("/api/ruler/grafana/api/v1/rules")),
|
|
48
|
-
);
|
|
49
6
|
|
|
50
|
-
|
|
51
|
-
"grafana_dashboard_list",
|
|
52
|
-
"Search dashboards.",
|
|
53
|
-
z.object({ query: z.string().optional() }),
|
|
54
|
-
async (p) => {
|
|
55
|
-
const q = p.query ?? "";
|
|
56
|
-
return jsonResult(await grafanaGet(`/api/search?type=dash-db&query=${encodeURIComponent(q)}`));
|
|
57
|
-
},
|
|
58
|
-
);
|
|
7
|
+
registerGrafanaTools(mcp);
|
|
59
8
|
|
|
60
9
|
const transport = new StdioServerTransport();
|
|
61
10
|
await mcp.connect(transport);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { envAuthHeaders, requiredEnv } from "../../../shared/env-json-api.ts";
|
|
3
|
+
import {
|
|
4
|
+
createRegisterSimpleTool,
|
|
5
|
+
createZodToolRegistrar,
|
|
6
|
+
mcpJsonResult as jsonResult,
|
|
7
|
+
} from "../../../shared/mcp-tool-kit.ts";
|
|
8
|
+
|
|
9
|
+
/** Only ONE trailing slash is dropped here — not `requiredBaseUrl`'s strip-them-all. */
|
|
10
|
+
function baseUrl(): string {
|
|
11
|
+
return requiredEnv("GRAFANA_URL").replace(/\/$/, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const authHeaders = envAuthHeaders({ env: "GRAFANA_API_TOKEN" });
|
|
15
|
+
|
|
16
|
+
async function grafanaGet(path: string): Promise<unknown> {
|
|
17
|
+
const pathPart = path.startsWith("/") ? path : `/${path}`;
|
|
18
|
+
const res = await fetch(`${baseUrl()}${pathPart}`, {
|
|
19
|
+
headers: authHeaders(),
|
|
20
|
+
});
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
throw new Error(`Grafana ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text) as unknown;
|
|
27
|
+
} catch {
|
|
28
|
+
return { raw: text };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
33
|
+
export const GRAFANA_TOOL_NAMES = ["grafana_alert_list", "grafana_dashboard_list"] as const;
|
|
34
|
+
|
|
35
|
+
export function registerGrafanaTools(server: { tool: (...args: never) => unknown }): void {
|
|
36
|
+
const reg = createZodToolRegistrar(createRegisterSimpleTool(server));
|
|
37
|
+
|
|
38
|
+
reg("grafana_alert_list", "List alert rules (Ruler API).", z.object({}), async () =>
|
|
39
|
+
jsonResult(await grafanaGet("/api/ruler/grafana/api/v1/rules")),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
reg(
|
|
43
|
+
"grafana_dashboard_list",
|
|
44
|
+
"Search dashboards.",
|
|
45
|
+
z.object({ query: z.string().optional() }),
|
|
46
|
+
async (p) => {
|
|
47
|
+
const q = p.query ?? "";
|
|
48
|
+
return jsonResult(
|
|
49
|
+
await grafanaGet(`/api/search?type=dash-db&query=${encodeURIComponent(q)}`),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
|
+
import { isRecord } from "../../../shared/cli-json-kit.ts";
|
|
4
|
+
import { requiredEnv } from "../../../shared/env-json-api.ts";
|
|
5
|
+
import { assertWithinDir, byExtension, walkFiles } from "../../../shared/walk-files.ts";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Tier-3 no-row-data reader for Great Expectations validation-result JSON
|
|
@@ -53,10 +56,6 @@ export interface GxExpectationMeta {
|
|
|
53
56
|
readonly sourceFile: string;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
57
|
-
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
59
|
function strField(r: Record<string, unknown>, key: string): string | null {
|
|
61
60
|
const v = r[key];
|
|
62
61
|
return typeof v === "string" && v !== "" ? v : null;
|
|
@@ -206,11 +205,7 @@ export function parseValidationResult(parsed: unknown, sourceFile: string): GxEx
|
|
|
206
205
|
* so the MCP tools surface a clear error rather than reading an empty path.
|
|
207
206
|
*/
|
|
208
207
|
export function resultsDir(): string {
|
|
209
|
-
|
|
210
|
-
if (dir === undefined || dir === "") {
|
|
211
|
-
throw new Error("GREAT_EXPECTATIONS_RESULTS_DIR is not set");
|
|
212
|
-
}
|
|
213
|
-
return resolve(dir);
|
|
208
|
+
return resolve(requiredEnv("GREAT_EXPECTATIONS_RESULTS_DIR"));
|
|
214
209
|
}
|
|
215
210
|
|
|
216
211
|
/**
|
|
@@ -219,42 +214,16 @@ export function resultsDir(): string {
|
|
|
219
214
|
* is the filesystem analog of the argv flag-smuggling guard.
|
|
220
215
|
*/
|
|
221
216
|
export function assertWithinResultsDir(candidate: string, root: string): void {
|
|
222
|
-
|
|
223
|
-
if (rel === "") {
|
|
224
|
-
return; // the dir itself
|
|
225
|
-
}
|
|
226
|
-
if (rel.startsWith("..") || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
227
|
-
throw new Error("path escapes the configured Great Expectations results dir");
|
|
228
|
-
}
|
|
217
|
+
assertWithinDir(candidate, root, "the configured Great Expectations results dir");
|
|
229
218
|
}
|
|
230
219
|
|
|
231
220
|
/** Recursively collect `*.json` artefact paths under `root` (bounded). */
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
let entries: import("node:fs").Dirent[];
|
|
239
|
-
try {
|
|
240
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
241
|
-
} catch {
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
for (const entry of entries) {
|
|
245
|
-
if (found.length >= MAX_FILES) {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const full = join(dir, entry.name);
|
|
249
|
-
if (entry.isDirectory()) {
|
|
250
|
-
await walk(full, depth + 1);
|
|
251
|
-
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".json")) {
|
|
252
|
-
found.push(full);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
await walk(root, 0);
|
|
257
|
-
return found;
|
|
221
|
+
function collectJsonFiles(root: string): Promise<string[]> {
|
|
222
|
+
return walkFiles(root, {
|
|
223
|
+
maxFiles: MAX_FILES,
|
|
224
|
+
maxDepth: MAX_WALK_DEPTH,
|
|
225
|
+
select: byExtension(".json"),
|
|
226
|
+
});
|
|
258
227
|
}
|
|
259
228
|
|
|
260
229
|
async function readArtefact(path: string): Promise<unknown> {
|
|
@@ -282,7 +251,7 @@ export async function listAllExpectations(): Promise<GxExpectationMeta[]> {
|
|
|
282
251
|
const out: GxExpectationMeta[] = [];
|
|
283
252
|
for (const file of files) {
|
|
284
253
|
assertWithinResultsDir(file, root);
|
|
285
|
-
const parsed = await readArtefact(file);
|
|
254
|
+
const parsed = await readArtefact(file); // NOSONAR S9382: one artefact at a time on purpose — up to MAX_FILES (1000) files, each read whole before the 4 MiB cap is checked, so a fan-out would open a thousand descriptors and buffer every artefact at once.
|
|
286
255
|
if (parsed === null) {
|
|
287
256
|
continue;
|
|
288
257
|
}
|
|
@@ -1,67 +1,6 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { matchesResult, searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
|
|
3
|
-
import {
|
|
4
|
-
encodeBasicAuthHeader,
|
|
5
|
-
mcpJsonResult as jsonResult,
|
|
6
|
-
} from "../../../shared/mcp-tool-kit.ts";
|
|
7
1
|
import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const BASE = "https://harvest.greenhouse.io";
|
|
11
|
-
|
|
12
|
-
function apiKey(): string {
|
|
13
|
-
const t = process.env["GREENHOUSE_API_KEY"]?.trim();
|
|
14
|
-
if (t === undefined || t === "") {
|
|
15
|
-
throw new Error("GREENHOUSE_API_KEY is not set");
|
|
16
|
-
}
|
|
17
|
-
return t;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function authHeader(): Record<string, string> {
|
|
21
|
-
return { Authorization: encodeBasicAuthHeader(apiKey(), ""), Accept: "application/json" };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function greenhouseGet(path: string): Promise<unknown> {
|
|
25
|
-
const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
|
|
26
|
-
const text = await res.text();
|
|
27
|
-
if (!res.ok) {
|
|
28
|
-
throw new Error(`Greenhouse ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
29
|
-
}
|
|
30
|
-
return JSON.parse(text) as unknown;
|
|
31
|
-
}
|
|
2
|
+
import { registerGreenhouseTools } from "./tools.ts";
|
|
32
3
|
|
|
33
4
|
await runReadOnlyMcpConnector("nimbus-greenhouse", (reg) => {
|
|
34
|
-
reg
|
|
35
|
-
"greenhouse_list",
|
|
36
|
-
"List the company's Greenhouse Harvest job openings (`GET /v1/jobs?per_page=100`). Returns a bare JSON array of job objects (NOT an envelope), each carrying the name, status, requisition id, departments, and offices.",
|
|
37
|
-
z.object({
|
|
38
|
-
per_page: z.number().int().min(1).max(100).optional(),
|
|
39
|
-
}),
|
|
40
|
-
async (p) => {
|
|
41
|
-
const search = new URLSearchParams({ per_page: String(p.per_page ?? 100), page: "1" });
|
|
42
|
-
return jsonResult(await greenhouseGet(`/v1/jobs?${search.toString()}`));
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
reg(
|
|
47
|
-
"greenhouse_get",
|
|
48
|
-
"Fetch one Greenhouse job by its id (`GET /v1/jobs/{id}`). Returns the job object directly. Throws when no match is found.",
|
|
49
|
-
z.object({
|
|
50
|
-
id: z.string().min(1),
|
|
51
|
-
}),
|
|
52
|
-
async (p) => {
|
|
53
|
-
return jsonResult(await greenhouseGet(`/v1/jobs/${encodeURIComponent(p.id)}`));
|
|
54
|
-
},
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
reg(
|
|
58
|
-
"greenhouse_search",
|
|
59
|
-
"Substring search across the company's Greenhouse job openings (first page only). Matches the query against the job name, status, requisition id, the department names, and the office names + locations (case-insensitive). Returns a `{ matches: [...] }` envelope.",
|
|
60
|
-
searchToolInputSchema(100),
|
|
61
|
-
async (p) => {
|
|
62
|
-
const search = new URLSearchParams({ per_page: "100", page: "1" });
|
|
63
|
-
const root = await greenhouseGet(`/v1/jobs?${search.toString()}`);
|
|
64
|
-
return matchesResult(root, filterGreenhouseJobs, p);
|
|
65
|
-
},
|
|
66
|
-
);
|
|
5
|
+
registerGreenhouseTools(reg);
|
|
67
6
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createJsonGetter, requiredEnv } from "../../../shared/env-json-api.ts";
|
|
3
|
+
import { matchesResult, searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
|
|
4
|
+
import {
|
|
5
|
+
encodeBasicAuthHeader,
|
|
6
|
+
mcpJsonResult as jsonResult,
|
|
7
|
+
} from "../../../shared/mcp-tool-kit.ts";
|
|
8
|
+
import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
|
|
9
|
+
import { filterGreenhouseJobs } from "./search-filter.ts";
|
|
10
|
+
|
|
11
|
+
const BASE = "https://harvest.greenhouse.io";
|
|
12
|
+
|
|
13
|
+
/** HTTP Basic with the API key as the username and an empty password. */
|
|
14
|
+
function authHeader(): Record<string, string> {
|
|
15
|
+
return {
|
|
16
|
+
Authorization: encodeBasicAuthHeader(requiredEnv("GREENHOUSE_API_KEY"), ""),
|
|
17
|
+
Accept: "application/json",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const greenhouseGet = createJsonGetter({
|
|
22
|
+
base: BASE,
|
|
23
|
+
label: "Greenhouse",
|
|
24
|
+
headers: authHeader,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
28
|
+
export const GREENHOUSE_TOOL_NAMES = [
|
|
29
|
+
"greenhouse_list",
|
|
30
|
+
"greenhouse_get",
|
|
31
|
+
"greenhouse_search",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export function registerGreenhouseTools(reg: ZodToolRegistrar): void {
|
|
35
|
+
reg(
|
|
36
|
+
"greenhouse_list",
|
|
37
|
+
"List the company's Greenhouse Harvest job openings (`GET /v1/jobs?per_page=100`). Returns a bare JSON array of job objects (NOT an envelope), each carrying the name, status, requisition id, departments, and offices.",
|
|
38
|
+
z.object({
|
|
39
|
+
per_page: z.number().int().min(1).max(100).optional(),
|
|
40
|
+
}),
|
|
41
|
+
async (p) => {
|
|
42
|
+
const search = new URLSearchParams({ per_page: String(p.per_page ?? 100), page: "1" });
|
|
43
|
+
return jsonResult(await greenhouseGet(`/v1/jobs?${search.toString()}`));
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
reg(
|
|
48
|
+
"greenhouse_get",
|
|
49
|
+
"Fetch one Greenhouse job by its id (`GET /v1/jobs/{id}`). Returns the job object directly. Throws when no match is found.",
|
|
50
|
+
z.object({
|
|
51
|
+
id: z.string().min(1),
|
|
52
|
+
}),
|
|
53
|
+
async (p) => {
|
|
54
|
+
return jsonResult(await greenhouseGet(`/v1/jobs/${encodeURIComponent(p.id)}`));
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
reg(
|
|
59
|
+
"greenhouse_search",
|
|
60
|
+
"Substring search across the company's Greenhouse job openings (first page only). Matches the query against the job name, status, requisition id, the department names, and the office names + locations (case-insensitive). Returns a `{ matches: [...] }` envelope.",
|
|
61
|
+
searchToolInputSchema(100),
|
|
62
|
+
async (p) => {
|
|
63
|
+
const search = new URLSearchParams({ per_page: "100", page: "1" });
|
|
64
|
+
const root = await greenhouseGet(`/v1/jobs?${search.toString()}`);
|
|
65
|
+
return matchesResult(root, filterGreenhouseJobs, p);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|