@oliverames/mcp-server-for-wave 1.0.0
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/LICENSE +23 -0
- package/README.md +434 -0
- package/assets/icon.png +0 -0
- package/docs/hosted-oauth-connector.md +54 -0
- package/docs/privacy.md +50 -0
- package/index.js +5816 -0
- package/package.json +61 -0
- package/scripts/build-mcpb.mjs +121 -0
- package/scripts/check-release-consistency.mjs +127 -0
- package/scripts/smoke-list-tools.mjs +112 -0
- package/scripts/smoke-packed-install.mjs +139 -0
- package/scripts/smoke-validate-graphql.mjs +164 -0
- package/scripts/sync-plugin-metadata.mjs +94 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema-check every GraphQL document against the live Wave API.
|
|
5
|
+
*
|
|
6
|
+
* Wave validates a document and coerces its variables *before* it checks
|
|
7
|
+
* authentication, so an unauthenticated request tells the two apart:
|
|
8
|
+
*
|
|
9
|
+
* UNAUTHENTICATED -> document and variables are valid
|
|
10
|
+
* GRAPHQL_VALIDATION_FAILED -> the document is wrong
|
|
11
|
+
* BAD_USER_INPUT -> a variable is the wrong type
|
|
12
|
+
*
|
|
13
|
+
* That makes the whole surface verifiable in CI with no credentials. Variables
|
|
14
|
+
* are synthesized from Wave's own introspected schema, so every required input
|
|
15
|
+
* field gets a type-correct placeholder.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import process from "node:process";
|
|
19
|
+
|
|
20
|
+
const ENDPOINT = "https://gql.waveapps.com/graphql/public";
|
|
21
|
+
const REQUEST_SPACING_MS = 120; // stay well clear of Wave's rate limit
|
|
22
|
+
|
|
23
|
+
const INTROSPECTION = `
|
|
24
|
+
query IntrospectionQuery {
|
|
25
|
+
__schema {
|
|
26
|
+
types {
|
|
27
|
+
kind
|
|
28
|
+
name
|
|
29
|
+
inputFields { name type { ...TypeRef } }
|
|
30
|
+
enumValues(includeDeprecated: true) { name }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
fragment TypeRef on __Type {
|
|
35
|
+
kind
|
|
36
|
+
name
|
|
37
|
+
ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } }
|
|
38
|
+
}`;
|
|
39
|
+
|
|
40
|
+
const SCALAR_SAMPLES = {
|
|
41
|
+
ID: "1",
|
|
42
|
+
String: "x",
|
|
43
|
+
Int: 1,
|
|
44
|
+
Float: 1.0,
|
|
45
|
+
Boolean: true,
|
|
46
|
+
Decimal: "1.00",
|
|
47
|
+
Date: "2026-01-01",
|
|
48
|
+
DateTime: "2026-01-01T00:00:00Z",
|
|
49
|
+
URL: "https://example.com",
|
|
50
|
+
JSON: {},
|
|
51
|
+
HexColorCode: "#000000",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
async function post(body) {
|
|
55
|
+
const response = await fetch(ENDPOINT, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "Content-Type": "application/json" },
|
|
58
|
+
body: JSON.stringify(body),
|
|
59
|
+
});
|
|
60
|
+
return response.json();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loadSchema() {
|
|
64
|
+
const result = await post({ query: INTROSPECTION });
|
|
65
|
+
if (result.errors) {
|
|
66
|
+
throw new Error(`Introspection failed: ${result.errors.map((e) => e.message).join("; ")}`);
|
|
67
|
+
}
|
|
68
|
+
return new Map(result.data.__schema.types.map((t) => [t.name, t]));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Parse a variable type from a GraphQL document header, e.g. "[InvoiceSort!]!". */
|
|
72
|
+
function parseType(text) {
|
|
73
|
+
const trimmed = text.trim();
|
|
74
|
+
if (trimmed.endsWith("!")) return { kind: "NON_NULL", ofType: parseType(trimmed.slice(0, -1)) };
|
|
75
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
76
|
+
return { kind: "LIST", ofType: parseType(trimmed.slice(1, -1)) };
|
|
77
|
+
}
|
|
78
|
+
return { kind: "NAMED", name: trimmed };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sampleFor(types, typeRef, depth = 0) {
|
|
82
|
+
if (typeRef.kind === "NON_NULL") return sampleFor(types, typeRef.ofType, depth);
|
|
83
|
+
if (typeRef.kind === "LIST") return [sampleFor(types, typeRef.ofType, depth + 1)];
|
|
84
|
+
|
|
85
|
+
const definition = types.get(typeRef.name);
|
|
86
|
+
if (definition?.kind === "ENUM") return definition.enumValues[0].name;
|
|
87
|
+
if (definition?.kind === "INPUT_OBJECT") {
|
|
88
|
+
if (depth > 6) return {};
|
|
89
|
+
const value = {};
|
|
90
|
+
for (const field of definition.inputFields ?? []) {
|
|
91
|
+
// Only required fields, to keep payloads minimal and unambiguous.
|
|
92
|
+
if (field.type.kind === "NON_NULL") value[field.name] = sampleFor(types, field.type, depth + 1);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
return SCALAR_SAMPLES[typeRef.name] ?? "x";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function variablesFor(types, document) {
|
|
100
|
+
const header = document.split("{")[0];
|
|
101
|
+
const variables = {};
|
|
102
|
+
for (const match of header.matchAll(/\$(\w+)\s*:\s*([[\]\w!]+)/g)) {
|
|
103
|
+
variables[match[1]] = sampleFor(types, parseType(match[2]));
|
|
104
|
+
}
|
|
105
|
+
return variables;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function operationName(document) {
|
|
109
|
+
return document.match(/^\s*(?:query|mutation)\s+(\w+)/)?.[1] ?? "(anonymous)";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
// Importing the server registers every document as a side effect.
|
|
114
|
+
process.env.WAVE_MCP_NO_AUTOSTART = "1";
|
|
115
|
+
process.env.WAVE_DISABLE_AGENT_CONFIG_FALLBACK = "1";
|
|
116
|
+
const module = await import("../index.js");
|
|
117
|
+
module.createWaveServer({ getAccessToken: async () => null, hasCredentials: false, writesEnabled: true });
|
|
118
|
+
|
|
119
|
+
// The factory runs once at import for the stdio instance and once above, so
|
|
120
|
+
// the registry holds duplicates.
|
|
121
|
+
const documents = [...new Set(module.__testables.GRAPHQL_DOCUMENTS)];
|
|
122
|
+
console.log(`Validating ${documents.length} GraphQL documents against ${ENDPOINT}\n`);
|
|
123
|
+
|
|
124
|
+
const types = await loadSchema();
|
|
125
|
+
const failures = [];
|
|
126
|
+
|
|
127
|
+
for (const document of documents) {
|
|
128
|
+
const name = operationName(document);
|
|
129
|
+
const variables = variablesFor(types, document);
|
|
130
|
+
let body;
|
|
131
|
+
try {
|
|
132
|
+
body = await post({ query: document, variables });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.log(` ERROR ${name}: ${error.message}`);
|
|
135
|
+
failures.push({ name, detail: error.message });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const errors = body.errors ?? [];
|
|
140
|
+
const codes = new Set(errors.map((e) => e?.extensions?.code).filter(Boolean));
|
|
141
|
+
const onlyAuth = errors.length === 0 || (codes.size === 1 && codes.has("UNAUTHENTICATED"));
|
|
142
|
+
|
|
143
|
+
if (onlyAuth) {
|
|
144
|
+
console.log(` ok ${name}`);
|
|
145
|
+
} else {
|
|
146
|
+
const detail = `${[...codes].join(",")} ${errors.map((e) => e.message).join("; ")}`;
|
|
147
|
+
console.log(` FAIL ${name}\n ${detail}`);
|
|
148
|
+
failures.push({ name, detail });
|
|
149
|
+
}
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, REQUEST_SPACING_MS));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(`\n${documents.length - failures.length}/${documents.length} documents valid`);
|
|
154
|
+
if (failures.length) {
|
|
155
|
+
console.error("\nFailures:");
|
|
156
|
+
for (const failure of failures) console.error(` ${failure.name}: ${failure.detail}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
main().catch((error) => {
|
|
162
|
+
console.error(error.message);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Propagate package.json's version and package name into every plugin manifest.
|
|
5
|
+
*
|
|
6
|
+
* Five hosts each want their own manifest, so a hand-bumped release drifts
|
|
7
|
+
* almost immediately. `npm version` runs this and stages the results.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const projectRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
15
|
+
const packageJson = readJson("package.json");
|
|
16
|
+
const { version, name: packageName } = packageJson;
|
|
17
|
+
|
|
18
|
+
const pluginName = "wave-mcp-server";
|
|
19
|
+
const packageInstallTarget = `${packageName}@latest`;
|
|
20
|
+
|
|
21
|
+
const pluginManifestPaths = [
|
|
22
|
+
".claude-plugin/plugin.json",
|
|
23
|
+
"codex/.codex-plugin/plugin.json",
|
|
24
|
+
".hermes-plugin/plugin.json",
|
|
25
|
+
".antigravity-plugin/plugin.json",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const marketplacePaths = [
|
|
29
|
+
".claude-plugin/marketplace.json",
|
|
30
|
+
".agents/plugins/marketplace.json",
|
|
31
|
+
".hermes-plugin/marketplace.json",
|
|
32
|
+
".antigravity-plugin/marketplace.json",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const mcpConfigPaths = [
|
|
36
|
+
".mcp.json",
|
|
37
|
+
"codex/.codex-plugin/mcp.json",
|
|
38
|
+
".hermes-plugin/mcp.json",
|
|
39
|
+
".antigravity-plugin/mcp_config.json",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const manifestPath of pluginManifestPaths) {
|
|
43
|
+
updateJson(manifestPath, (data) => {
|
|
44
|
+
data.version = version;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const marketplacePath of marketplacePaths) {
|
|
49
|
+
updateJson(marketplacePath, (data) => {
|
|
50
|
+
for (const plugin of data.plugins ?? []) {
|
|
51
|
+
if (plugin.name === pluginName) plugin.version = version;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const mcpConfigPath of mcpConfigPaths) {
|
|
57
|
+
updateJson(mcpConfigPath, (data) => {
|
|
58
|
+
setPackageInstallTarget(findMcpServer(data));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The server version reported in the MCP handshake must match the package, or
|
|
63
|
+
// clients advertise a version that was never released.
|
|
64
|
+
updateSourceVersion();
|
|
65
|
+
|
|
66
|
+
console.log(`Synced plugin metadata to version ${version}.`);
|
|
67
|
+
|
|
68
|
+
function findMcpServer(data) {
|
|
69
|
+
return data.mcpServers?.[pluginName] ?? data[pluginName];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setPackageInstallTarget(server) {
|
|
73
|
+
if (!server || !Array.isArray(server.args)) return;
|
|
74
|
+
const index = server.args.findIndex((arg) => typeof arg === "string" && /^@oliverames\/.+@latest$/.test(arg));
|
|
75
|
+
if (index >= 0) server.args[index] = packageInstallTarget;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateSourceVersion() {
|
|
79
|
+
const indexPath = path.join(projectRoot, "index.js");
|
|
80
|
+
const source = fs.readFileSync(indexPath, "utf8");
|
|
81
|
+
const updated = source.replace(/const SERVER_VERSION = "[^"]*";/, `const SERVER_VERSION = "${version}";`);
|
|
82
|
+
if (updated !== source) fs.writeFileSync(indexPath, updated);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readJson(relativePath) {
|
|
86
|
+
return JSON.parse(fs.readFileSync(path.join(projectRoot, relativePath), "utf8"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function updateJson(relativePath, update) {
|
|
90
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
91
|
+
const data = JSON.parse(fs.readFileSync(fullPath, "utf8"));
|
|
92
|
+
update(data);
|
|
93
|
+
fs.writeFileSync(fullPath, `${JSON.stringify(data, null, 2)}\n`);
|
|
94
|
+
}
|