@fitzzero/quickdraw-core 2.0.0 → 2.1.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/dist/client/index.d.ts +97 -1
- package/dist/client/index.js +210 -1
- package/dist/client/index.js.map +1 -1
- package/dist/server/index.d.ts +160 -1
- package/dist/server/index.js +675 -1
- package/dist/server/index.js.map +1 -1
- package/eslint-plugin-quickdraw/index.mjs +14 -0
- package/eslint-plugin-quickdraw/rules/no-direct-prisma-mutations.mjs +49 -0
- package/eslint-plugin-quickdraw/rules/no-service-method-record.mjs +35 -0
- package/eslint-plugin-quickdraw/rules/no-unsafe-payload-cast.mjs +41 -0
- package/eslint-plugin-quickdraw/rules/require-zod-schema.mjs +60 -0
- package/package.json +6 -2
package/dist/server/index.js
CHANGED
|
@@ -2,6 +2,9 @@ import express from 'express';
|
|
|
2
2
|
import { createServer } from 'http';
|
|
3
3
|
import { Server } from 'socket.io';
|
|
4
4
|
import * as jose from 'jose';
|
|
5
|
+
import * as readline from 'readline';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
5
8
|
|
|
6
9
|
// src/shared/types.ts
|
|
7
10
|
var consoleLogger = {
|
|
@@ -1669,6 +1672,677 @@ function requireEnv(name, defaultValue) {
|
|
|
1669
1672
|
return value;
|
|
1670
1673
|
}
|
|
1671
1674
|
|
|
1672
|
-
|
|
1675
|
+
// src/server/mcp/McpRegistry.ts
|
|
1676
|
+
var McpRegistry = class {
|
|
1677
|
+
services = /* @__PURE__ */ new Map();
|
|
1678
|
+
methodCache = /* @__PURE__ */ new Map();
|
|
1679
|
+
hydrateUserContext;
|
|
1680
|
+
logger;
|
|
1681
|
+
customToolSpecs = null;
|
|
1682
|
+
constructor(options) {
|
|
1683
|
+
this.hydrateUserContext = options.hydrateUserContext;
|
|
1684
|
+
this.logger = options.logger?.child({ service: "McpRegistry" }) ?? consoleLogger.child({ service: "McpRegistry" });
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Optionally provide pre-generated tool specs for richer descriptions.
|
|
1688
|
+
* If not set, tool definitions are built dynamically from registered services.
|
|
1689
|
+
*/
|
|
1690
|
+
setToolSpecs(specs) {
|
|
1691
|
+
this.customToolSpecs = specs;
|
|
1692
|
+
}
|
|
1693
|
+
registerService(serviceName, serviceInstance) {
|
|
1694
|
+
this.services.set(serviceName, serviceInstance);
|
|
1695
|
+
const methods = serviceInstance.getPublicMethods?.() ?? [];
|
|
1696
|
+
const methodMap = /* @__PURE__ */ new Map();
|
|
1697
|
+
for (const method of methods) {
|
|
1698
|
+
methodMap.set(method.name, method);
|
|
1699
|
+
}
|
|
1700
|
+
this.methodCache.set(serviceName, methodMap);
|
|
1701
|
+
this.logger.info(`Registered MCP service: ${serviceName}`, {
|
|
1702
|
+
category: "startup",
|
|
1703
|
+
methodCount: methods.length,
|
|
1704
|
+
methods: methods.map((m) => m.name)
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
listTools(options) {
|
|
1708
|
+
const hasDefaultUser = !!options?.defaultUserId;
|
|
1709
|
+
if (this.customToolSpecs) {
|
|
1710
|
+
return this.customToolSpecs.filter((s) => this.services.has(s.name)).map((svc) => this.buildToolDefinition(svc.name, svc.description, svc.methodNames, hasDefaultUser));
|
|
1711
|
+
}
|
|
1712
|
+
const tools = [];
|
|
1713
|
+
for (const [serviceName] of this.services) {
|
|
1714
|
+
const methodMap = this.methodCache.get(serviceName);
|
|
1715
|
+
if (!methodMap) continue;
|
|
1716
|
+
const methodNames = Array.from(methodMap.keys());
|
|
1717
|
+
const description = this.buildDescription(serviceName, methodNames);
|
|
1718
|
+
tools.push(this.buildToolDefinition(serviceName, description, methodNames, hasDefaultUser));
|
|
1719
|
+
}
|
|
1720
|
+
return tools;
|
|
1721
|
+
}
|
|
1722
|
+
async invoke(serviceName, methodName, payload, userId) {
|
|
1723
|
+
const { instance, method } = this.resolveServiceMethod(serviceName, methodName);
|
|
1724
|
+
const { serviceAccess } = await this.hydrateUserContext(userId);
|
|
1725
|
+
const ctx = {
|
|
1726
|
+
id: `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1727
|
+
userId,
|
|
1728
|
+
serviceAccess,
|
|
1729
|
+
connected: true,
|
|
1730
|
+
disconnected: false
|
|
1731
|
+
};
|
|
1732
|
+
const entryId = this.resolveEntryId(method, payload);
|
|
1733
|
+
const logTag = `${serviceName}.${methodName}`;
|
|
1734
|
+
this.logger.info(`MCP invoke start: ${logTag}`, {
|
|
1735
|
+
category: "request_processing",
|
|
1736
|
+
source: "mcp",
|
|
1737
|
+
serviceName,
|
|
1738
|
+
methodName,
|
|
1739
|
+
userId,
|
|
1740
|
+
entryId
|
|
1741
|
+
});
|
|
1742
|
+
const startTime = Date.now();
|
|
1743
|
+
try {
|
|
1744
|
+
const validatedPayload = this.validatePayload(method, payload);
|
|
1745
|
+
if (instance.ensureAccessForMethod) {
|
|
1746
|
+
await instance.ensureAccessForMethod(
|
|
1747
|
+
method.access,
|
|
1748
|
+
ctx,
|
|
1749
|
+
entryId
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
const result = await method.handler(validatedPayload, {
|
|
1753
|
+
userId,
|
|
1754
|
+
socketId: ctx.id,
|
|
1755
|
+
serviceAccess
|
|
1756
|
+
});
|
|
1757
|
+
this.logger.info(`MCP invoke success: ${logTag}`, {
|
|
1758
|
+
category: "request_processing",
|
|
1759
|
+
outcome: "success",
|
|
1760
|
+
source: "mcp",
|
|
1761
|
+
serviceName,
|
|
1762
|
+
methodName,
|
|
1763
|
+
userId,
|
|
1764
|
+
durationMs: Date.now() - startTime
|
|
1765
|
+
});
|
|
1766
|
+
return result;
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
this.logger.error(`MCP invoke error: ${logTag}`, {
|
|
1769
|
+
category: "error",
|
|
1770
|
+
source: "mcp",
|
|
1771
|
+
serviceName,
|
|
1772
|
+
methodName,
|
|
1773
|
+
userId,
|
|
1774
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1775
|
+
durationMs: Date.now() - startTime
|
|
1776
|
+
});
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
getServices() {
|
|
1781
|
+
return Array.from(this.services.keys());
|
|
1782
|
+
}
|
|
1783
|
+
resolveServiceMethod(serviceName, methodName) {
|
|
1784
|
+
const instance = this.services.get(serviceName);
|
|
1785
|
+
if (!instance) {
|
|
1786
|
+
throw new Error(
|
|
1787
|
+
`Unknown service: ${serviceName}. Available: ${Array.from(this.services.keys()).join(", ")}`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
const methodMap = this.methodCache.get(serviceName);
|
|
1791
|
+
const method = methodMap?.get(methodName);
|
|
1792
|
+
if (!method) {
|
|
1793
|
+
throw new Error(
|
|
1794
|
+
`Unknown method "${methodName}" on ${serviceName}. Available: ${Array.from(methodMap?.keys() ?? []).join(", ")}`
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
return { instance, method };
|
|
1798
|
+
}
|
|
1799
|
+
validatePayload(method, payload) {
|
|
1800
|
+
if (method.schema) {
|
|
1801
|
+
const result = method.schema.safeParse(payload);
|
|
1802
|
+
if (!result.success) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
`Validation error: ${result.error?.message ?? "invalid payload"}`
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
return result.data;
|
|
1808
|
+
}
|
|
1809
|
+
return payload;
|
|
1810
|
+
}
|
|
1811
|
+
resolveEntryId(method, payload) {
|
|
1812
|
+
if (method.resolveEntryId) {
|
|
1813
|
+
return method.resolveEntryId(payload) ?? void 0;
|
|
1814
|
+
}
|
|
1815
|
+
if (payload && typeof payload === "object" && "id" in payload) {
|
|
1816
|
+
const v = payload.id;
|
|
1817
|
+
return typeof v === "string" ? v : void 0;
|
|
1818
|
+
}
|
|
1819
|
+
return void 0;
|
|
1820
|
+
}
|
|
1821
|
+
buildToolDefinition(name, description, methodNames, hasDefaultUser) {
|
|
1822
|
+
return {
|
|
1823
|
+
name,
|
|
1824
|
+
description,
|
|
1825
|
+
inputSchema: {
|
|
1826
|
+
type: "object",
|
|
1827
|
+
properties: {
|
|
1828
|
+
method: {
|
|
1829
|
+
type: "string",
|
|
1830
|
+
enum: methodNames,
|
|
1831
|
+
description: "The service method to call"
|
|
1832
|
+
},
|
|
1833
|
+
payload: {
|
|
1834
|
+
type: "object",
|
|
1835
|
+
description: "Method-specific payload (see method listing in description for fields)"
|
|
1836
|
+
},
|
|
1837
|
+
userId: {
|
|
1838
|
+
type: "string",
|
|
1839
|
+
description: hasDefaultUser ? "User ID (optional - defaults to MCP_USER_ID env var)" : "The authenticated user ID (required for ACL checks)"
|
|
1840
|
+
}
|
|
1841
|
+
},
|
|
1842
|
+
required: hasDefaultUser ? ["method"] : ["method", "userId"]
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
buildDescription(serviceName, methodNames) {
|
|
1847
|
+
const displayName = serviceName.replace(/Service$/i, "").replace(/([a-z])([A-Z])/g, "$1 $2") + "Service";
|
|
1848
|
+
const methodList = methodNames.map((m) => `- ${m}`).join("\n");
|
|
1849
|
+
return `${displayName} service.
|
|
1850
|
+
|
|
1851
|
+
Methods:
|
|
1852
|
+
${methodList}`;
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
function createMcpStdioServer(options) {
|
|
1856
|
+
const { name, version, registry, defaultUserId } = options;
|
|
1857
|
+
const serverInfo = { name, version };
|
|
1858
|
+
const capabilities = { tools: {} };
|
|
1859
|
+
function getToolDefinitions() {
|
|
1860
|
+
return registry.listTools({
|
|
1861
|
+
defaultUserId: defaultUserId ?? process.env.MCP_USER_ID
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
async function handleRequest(request) {
|
|
1865
|
+
const { id, method, params } = request;
|
|
1866
|
+
try {
|
|
1867
|
+
switch (method) {
|
|
1868
|
+
case "initialize":
|
|
1869
|
+
return {
|
|
1870
|
+
jsonrpc: "2.0",
|
|
1871
|
+
id,
|
|
1872
|
+
result: {
|
|
1873
|
+
protocolVersion: "2024-11-05",
|
|
1874
|
+
serverInfo,
|
|
1875
|
+
capabilities
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
case "initialized":
|
|
1879
|
+
case "notifications/initialized":
|
|
1880
|
+
return null;
|
|
1881
|
+
case "tools/list":
|
|
1882
|
+
return {
|
|
1883
|
+
jsonrpc: "2.0",
|
|
1884
|
+
id,
|
|
1885
|
+
result: { tools: getToolDefinitions() }
|
|
1886
|
+
};
|
|
1887
|
+
case "tools/call": {
|
|
1888
|
+
const { name: serviceName, arguments: args } = params;
|
|
1889
|
+
const methodName = args?.method;
|
|
1890
|
+
if (!methodName) {
|
|
1891
|
+
return {
|
|
1892
|
+
jsonrpc: "2.0",
|
|
1893
|
+
id,
|
|
1894
|
+
error: {
|
|
1895
|
+
code: -32602,
|
|
1896
|
+
message: `Missing required argument "method" for service ${serviceName}`
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
const userId = args?.userId ?? defaultUserId ?? process.env.MCP_USER_ID ?? "mcp-anonymous";
|
|
1901
|
+
const payload = args?.payload;
|
|
1902
|
+
const result = await registry.invoke(
|
|
1903
|
+
serviceName,
|
|
1904
|
+
methodName,
|
|
1905
|
+
payload,
|
|
1906
|
+
userId
|
|
1907
|
+
);
|
|
1908
|
+
return {
|
|
1909
|
+
jsonrpc: "2.0",
|
|
1910
|
+
id,
|
|
1911
|
+
result: {
|
|
1912
|
+
content: [
|
|
1913
|
+
{
|
|
1914
|
+
type: "text",
|
|
1915
|
+
text: JSON.stringify(result, null, 2)
|
|
1916
|
+
}
|
|
1917
|
+
]
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
case "ping":
|
|
1922
|
+
return { jsonrpc: "2.0", id, result: {} };
|
|
1923
|
+
default:
|
|
1924
|
+
return {
|
|
1925
|
+
jsonrpc: "2.0",
|
|
1926
|
+
id,
|
|
1927
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
return {
|
|
1932
|
+
jsonrpc: "2.0",
|
|
1933
|
+
id,
|
|
1934
|
+
error: {
|
|
1935
|
+
code: -32e3,
|
|
1936
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
async function main() {
|
|
1942
|
+
const rl = readline.createInterface({
|
|
1943
|
+
input: process.stdin,
|
|
1944
|
+
output: process.stdout,
|
|
1945
|
+
terminal: false
|
|
1946
|
+
});
|
|
1947
|
+
function send(response) {
|
|
1948
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
1949
|
+
}
|
|
1950
|
+
for await (const line of rl) {
|
|
1951
|
+
if (!line.trim()) continue;
|
|
1952
|
+
try {
|
|
1953
|
+
const request = JSON.parse(line);
|
|
1954
|
+
const response = await handleRequest(request);
|
|
1955
|
+
if (response !== null) {
|
|
1956
|
+
send(response);
|
|
1957
|
+
}
|
|
1958
|
+
} catch {
|
|
1959
|
+
send({
|
|
1960
|
+
jsonrpc: "2.0",
|
|
1961
|
+
id: null,
|
|
1962
|
+
error: { code: -32700, message: "Parse error" }
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
main().catch((error) => {
|
|
1968
|
+
process.stderr.write(`Fatal error: ${error}
|
|
1969
|
+
`);
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// src/server/mcp/McpBootstrap.ts
|
|
1975
|
+
function bootstrapMcpServer(serverModulePath) {
|
|
1976
|
+
process.env.MCP_MODE = "true";
|
|
1977
|
+
process.env.LOG_LEVEL = process.env.LOG_LEVEL ?? "warn";
|
|
1978
|
+
const stderrWrite = (data) => {
|
|
1979
|
+
process.stderr.write(data + "\n");
|
|
1980
|
+
};
|
|
1981
|
+
console.log = (...args) => {
|
|
1982
|
+
stderrWrite(args.map(String).join(" "));
|
|
1983
|
+
};
|
|
1984
|
+
console.info = (...args) => {
|
|
1985
|
+
stderrWrite(args.map(String).join(" "));
|
|
1986
|
+
};
|
|
1987
|
+
console.warn = (...args) => {
|
|
1988
|
+
stderrWrite(args.map(String).join(" "));
|
|
1989
|
+
};
|
|
1990
|
+
console.debug = (...args) => {
|
|
1991
|
+
stderrWrite(args.map(String).join(" "));
|
|
1992
|
+
};
|
|
1993
|
+
import(serverModulePath).catch((error) => {
|
|
1994
|
+
process.stderr.write(`Bootstrap error: ${error}
|
|
1995
|
+
`);
|
|
1996
|
+
process.exit(1);
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// src/server/mcp/McpHttpRoutes.ts
|
|
2001
|
+
function createMcpRoutes(router, options) {
|
|
2002
|
+
const { registry, verifyToken } = options;
|
|
2003
|
+
const logger = options.logger?.child({ service: "McpHttpRoutes" }) ?? consoleLogger.child({ service: "McpHttpRoutes" });
|
|
2004
|
+
router.get("/mcp/tools", (_req, res) => {
|
|
2005
|
+
const tools = registry.listTools();
|
|
2006
|
+
res.json({ tools });
|
|
2007
|
+
});
|
|
2008
|
+
router.post("/mcp/invoke", async (req, res) => {
|
|
2009
|
+
try {
|
|
2010
|
+
const authHeader = req.headers.authorization;
|
|
2011
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
2012
|
+
res.status(401).json({ error: "Missing or invalid Authorization header" });
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const token = authHeader.slice(7);
|
|
2016
|
+
const tokenPayload = await verifyToken(token);
|
|
2017
|
+
if (!tokenPayload?.userId) {
|
|
2018
|
+
res.status(401).json({ error: "Invalid token" });
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
const {
|
|
2022
|
+
service,
|
|
2023
|
+
method,
|
|
2024
|
+
payload: toolPayload
|
|
2025
|
+
} = req.body;
|
|
2026
|
+
if (!service || !method) {
|
|
2027
|
+
res.status(400).json({ error: "Missing service or method" });
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
const result = await registry.invoke(
|
|
2031
|
+
service,
|
|
2032
|
+
method,
|
|
2033
|
+
toolPayload,
|
|
2034
|
+
tokenPayload.userId
|
|
2035
|
+
);
|
|
2036
|
+
res.json({ success: true, data: result });
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
2039
|
+
logger.error("MCP HTTP invoke error", { error: message });
|
|
2040
|
+
res.status(500).json({ error: message });
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
return router;
|
|
2044
|
+
}
|
|
2045
|
+
function extractFieldNames(schemaContent) {
|
|
2046
|
+
const fields = [];
|
|
2047
|
+
const lines = schemaContent.split("\n");
|
|
2048
|
+
for (const line of lines) {
|
|
2049
|
+
const fieldMatch = line.match(/^\s*(\w+)\s*:/);
|
|
2050
|
+
if (fieldMatch?.[1]) {
|
|
2051
|
+
const name = fieldMatch[1];
|
|
2052
|
+
const isOptional2 = line.includes(".optional()") || line.includes(".nullable()");
|
|
2053
|
+
fields.push(isOptional2 ? `${name}?` : name);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return fields;
|
|
2057
|
+
}
|
|
2058
|
+
function buildPayloadHint(fields) {
|
|
2059
|
+
if (!fields || fields.length === 0) return "";
|
|
2060
|
+
return `(${fields.join(", ")})`;
|
|
2061
|
+
}
|
|
2062
|
+
function parseServiceFile(filePath) {
|
|
2063
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
2064
|
+
const classMatch = content.match(/export class (\w+Service)/);
|
|
2065
|
+
if (!classMatch?.[1]) return null;
|
|
2066
|
+
const className = classMatch[1];
|
|
2067
|
+
const serviceName = className.charAt(0).toLowerCase() + className.slice(1);
|
|
2068
|
+
const aclMatch = content.match(/hasEntryACL:\s*(true|false)/);
|
|
2069
|
+
const hasEntryACL = aclMatch ? aclMatch[1] === "true" : false;
|
|
2070
|
+
let aclPattern = "No ACL";
|
|
2071
|
+
if (hasEntryACL) {
|
|
2072
|
+
if (content.includes("override async checkEntryACL")) {
|
|
2073
|
+
aclPattern = "Membership table";
|
|
2074
|
+
} else {
|
|
2075
|
+
aclPattern = "JSON ACL (default)";
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (content.includes("override checkAccess")) {
|
|
2079
|
+
aclPattern = "Custom (self-access pattern)";
|
|
2080
|
+
}
|
|
2081
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
2082
|
+
const schemaRegex = /const (\w+Schema) = z\.object\(\{([\s\S]*?)\}\);/g;
|
|
2083
|
+
let schemaMatch;
|
|
2084
|
+
while ((schemaMatch = schemaRegex.exec(content)) !== null) {
|
|
2085
|
+
if (!schemaMatch[1] || !schemaMatch[2]) continue;
|
|
2086
|
+
const schemaContent = schemaMatch[2].split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n ");
|
|
2087
|
+
schemas.set(schemaMatch[1], schemaContent);
|
|
2088
|
+
}
|
|
2089
|
+
const methods = [];
|
|
2090
|
+
const methodRegex = /this\.defineMethod\(\s*["'](\w+)["'],\s*["'](\w+)["']/g;
|
|
2091
|
+
let methodMatch;
|
|
2092
|
+
while ((methodMatch = methodRegex.exec(content)) !== null) {
|
|
2093
|
+
const methodName = methodMatch[1] ?? "";
|
|
2094
|
+
const accessLevel = methodMatch[2] ?? "Read";
|
|
2095
|
+
const methodStart = methodMatch.index;
|
|
2096
|
+
let parenDepth = 0;
|
|
2097
|
+
let methodEnd = methodStart;
|
|
2098
|
+
let foundStart = false;
|
|
2099
|
+
for (let i = methodStart; i < content.length; i++) {
|
|
2100
|
+
if (content[i] === "(") {
|
|
2101
|
+
parenDepth++;
|
|
2102
|
+
foundStart = true;
|
|
2103
|
+
} else if (content[i] === ")") {
|
|
2104
|
+
parenDepth--;
|
|
2105
|
+
if (foundStart && parenDepth === 0) {
|
|
2106
|
+
methodEnd = i;
|
|
2107
|
+
break;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
const methodBlock = content.substring(methodStart, methodEnd + 1);
|
|
2112
|
+
const hasSchema = methodBlock.includes("schema:");
|
|
2113
|
+
const schemaRef = methodBlock.match(/schema:\s*(\w+Schema)/);
|
|
2114
|
+
const hasResolver = methodBlock.includes("resolveEntryId:");
|
|
2115
|
+
let payloadFields;
|
|
2116
|
+
if (schemaRef?.[1]) {
|
|
2117
|
+
const schemaContent = schemas.get(schemaRef[1]);
|
|
2118
|
+
if (schemaContent) {
|
|
2119
|
+
payloadFields = extractFieldNames(schemaContent);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
methods.push({
|
|
2123
|
+
name: methodName,
|
|
2124
|
+
accessLevel,
|
|
2125
|
+
hasSchema,
|
|
2126
|
+
schemaName: schemaRef?.[1],
|
|
2127
|
+
hasResolver,
|
|
2128
|
+
payloadFields
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
return {
|
|
2132
|
+
name: serviceName,
|
|
2133
|
+
className,
|
|
2134
|
+
hasEntryACL,
|
|
2135
|
+
aclPattern,
|
|
2136
|
+
methods,
|
|
2137
|
+
schemas
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
function findServiceDirs(dir, basePath = "") {
|
|
2141
|
+
const results = [];
|
|
2142
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2143
|
+
for (const entry of entries) {
|
|
2144
|
+
if (entry.isDirectory()) {
|
|
2145
|
+
const fullPath = path.join(dir, entry.name);
|
|
2146
|
+
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
2147
|
+
if (fs.existsSync(path.join(fullPath, "index.ts"))) {
|
|
2148
|
+
results.push(relativePath);
|
|
2149
|
+
}
|
|
2150
|
+
const nested = findServiceDirs(fullPath, relativePath);
|
|
2151
|
+
results.push(...nested);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return results;
|
|
2155
|
+
}
|
|
2156
|
+
function generateToolsFileContent(services) {
|
|
2157
|
+
let content = `/**
|
|
2158
|
+
* MCP Tool Metadata
|
|
2159
|
+
*
|
|
2160
|
+
* Auto-generated from service definitions.
|
|
2161
|
+
* DO NOT EDIT - regenerate with: pnpm docs:generate
|
|
2162
|
+
*/
|
|
2163
|
+
|
|
2164
|
+
export type MethodSpec = {
|
|
2165
|
+
name: string;
|
|
2166
|
+
access: "Public" | "Read" | "Moderate" | "Admin";
|
|
2167
|
+
entryScoped: boolean;
|
|
2168
|
+
payloadHint: string;
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2171
|
+
export type ServiceToolSpec = {
|
|
2172
|
+
name: string;
|
|
2173
|
+
description: string;
|
|
2174
|
+
methods: MethodSpec[];
|
|
2175
|
+
methodNames: string[];
|
|
2176
|
+
};
|
|
2177
|
+
|
|
2178
|
+
`;
|
|
2179
|
+
const serviceSpecs = [];
|
|
2180
|
+
for (const service of services) {
|
|
2181
|
+
const methodSpecs = service.methods.map((method) => {
|
|
2182
|
+
const hint = buildPayloadHint(method.payloadFields);
|
|
2183
|
+
return ` {
|
|
2184
|
+
name: "${method.name}",
|
|
2185
|
+
access: "${method.accessLevel}" as const,
|
|
2186
|
+
entryScoped: ${method.hasResolver},
|
|
2187
|
+
payloadHint: "${hint}",
|
|
2188
|
+
}`;
|
|
2189
|
+
});
|
|
2190
|
+
const methodDescriptions = service.methods.map((method) => {
|
|
2191
|
+
const hint = buildPayloadHint(method.payloadFields);
|
|
2192
|
+
return `- ${method.name}${hint}`;
|
|
2193
|
+
}).join("\\n");
|
|
2194
|
+
const description = `${service.className} service.\\n\\nMethods:\\n${methodDescriptions}`;
|
|
2195
|
+
serviceSpecs.push(` {
|
|
2196
|
+
name: "${service.name}",
|
|
2197
|
+
description: "${description}",
|
|
2198
|
+
methods: [
|
|
2199
|
+
${methodSpecs.join(",\n")}
|
|
2200
|
+
],
|
|
2201
|
+
methodNames: [${service.methods.map((m) => `"${m.name}"`).join(", ")}],
|
|
2202
|
+
}`);
|
|
2203
|
+
}
|
|
2204
|
+
content += `export const SERVICE_TOOLS: ServiceToolSpec[] = [
|
|
2205
|
+
${serviceSpecs.join(",\n")}
|
|
2206
|
+
];
|
|
2207
|
+
|
|
2208
|
+
`;
|
|
2209
|
+
content += `export const SERVICE_TOOLS_BY_NAME: Record<string, ServiceToolSpec> = Object.fromEntries(
|
|
2210
|
+
SERVICE_TOOLS.map(s => [s.name, s])
|
|
2211
|
+
);
|
|
2212
|
+
|
|
2213
|
+
`;
|
|
2214
|
+
content += `export function getServiceNames(): string[] {
|
|
2215
|
+
return SERVICE_TOOLS.map(s => s.name);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
export function getServiceTool(serviceName: string): ServiceToolSpec | undefined {
|
|
2219
|
+
return SERVICE_TOOLS_BY_NAME[serviceName];
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
export function methodExists(serviceName: string, methodName: string): boolean {
|
|
2223
|
+
const service = SERVICE_TOOLS_BY_NAME[serviceName];
|
|
2224
|
+
return !!service?.methodNames.includes(methodName);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
export function getMethodSpec(serviceName: string, methodName: string): MethodSpec | undefined {
|
|
2228
|
+
const service = SERVICE_TOOLS_BY_NAME[serviceName];
|
|
2229
|
+
return service?.methods.find(m => m.name === methodName);
|
|
2230
|
+
}
|
|
2231
|
+
`;
|
|
2232
|
+
return content;
|
|
2233
|
+
}
|
|
2234
|
+
function generateServiceDoc(service) {
|
|
2235
|
+
let doc = `# ${service.className}
|
|
2236
|
+
|
|
2237
|
+
`;
|
|
2238
|
+
doc += `**Service Name:** \`${service.name}\`
|
|
2239
|
+
|
|
2240
|
+
`;
|
|
2241
|
+
doc += `**Access Control:** ${service.aclPattern}
|
|
2242
|
+
|
|
2243
|
+
`;
|
|
2244
|
+
if (service.hasEntryACL) {
|
|
2245
|
+
doc += `**Entry-level ACL:** Enabled
|
|
2246
|
+
|
|
2247
|
+
`;
|
|
2248
|
+
}
|
|
2249
|
+
doc += `---
|
|
2250
|
+
|
|
2251
|
+
## Methods
|
|
2252
|
+
|
|
2253
|
+
`;
|
|
2254
|
+
for (const method of service.methods) {
|
|
2255
|
+
doc += `### ${method.name}
|
|
2256
|
+
|
|
2257
|
+
`;
|
|
2258
|
+
doc += `- **Access Level:** \`${method.accessLevel}\`
|
|
2259
|
+
`;
|
|
2260
|
+
doc += `- **Socket Event:** \`${service.name}:${method.name}\`
|
|
2261
|
+
`;
|
|
2262
|
+
if (method.hasResolver) {
|
|
2263
|
+
doc += `- **Entry-scoped:** Yes
|
|
2264
|
+
`;
|
|
2265
|
+
}
|
|
2266
|
+
if (method.hasSchema && method.schemaName) {
|
|
2267
|
+
doc += `- **Validation:** Zod schema (\`${method.schemaName}\`)
|
|
2268
|
+
`;
|
|
2269
|
+
const schemaContent = service.schemas.get(method.schemaName);
|
|
2270
|
+
if (schemaContent) {
|
|
2271
|
+
doc += `
|
|
2272
|
+
**Payload Schema:**
|
|
2273
|
+
|
|
2274
|
+
\`\`\`typescript
|
|
2275
|
+
{
|
|
2276
|
+
${schemaContent}
|
|
2277
|
+
}
|
|
2278
|
+
\`\`\`
|
|
2279
|
+
`;
|
|
2280
|
+
}
|
|
2281
|
+
} else {
|
|
2282
|
+
doc += `- **Validation:** None
|
|
2283
|
+
`;
|
|
2284
|
+
}
|
|
2285
|
+
doc += `
|
|
2286
|
+
`;
|
|
2287
|
+
}
|
|
2288
|
+
doc += `---
|
|
2289
|
+
|
|
2290
|
+
*Generated by quickdraw-core*
|
|
2291
|
+
`;
|
|
2292
|
+
return doc;
|
|
2293
|
+
}
|
|
2294
|
+
function generateToolMetadata(options) {
|
|
2295
|
+
const { servicesDir, toolsOutputPath, docsOutputDir } = options;
|
|
2296
|
+
const serviceDirs = findServiceDirs(servicesDir);
|
|
2297
|
+
if (docsOutputDir && !fs.existsSync(docsOutputDir)) {
|
|
2298
|
+
fs.mkdirSync(docsOutputDir, { recursive: true });
|
|
2299
|
+
}
|
|
2300
|
+
const toolsDir = path.dirname(toolsOutputPath);
|
|
2301
|
+
if (!fs.existsSync(toolsDir)) {
|
|
2302
|
+
fs.mkdirSync(toolsDir, { recursive: true });
|
|
2303
|
+
}
|
|
2304
|
+
const allServices = [];
|
|
2305
|
+
for (const serviceDir of serviceDirs) {
|
|
2306
|
+
const indexPath = path.join(servicesDir, serviceDir, "index.ts");
|
|
2307
|
+
if (!fs.existsSync(indexPath)) continue;
|
|
2308
|
+
const serviceInfo = parseServiceFile(indexPath);
|
|
2309
|
+
if (!serviceInfo) continue;
|
|
2310
|
+
allServices.push(serviceInfo);
|
|
2311
|
+
if (docsOutputDir) {
|
|
2312
|
+
const doc = generateServiceDoc(serviceInfo);
|
|
2313
|
+
const outputPath = path.join(
|
|
2314
|
+
docsOutputDir,
|
|
2315
|
+
`${serviceInfo.className}.md`
|
|
2316
|
+
);
|
|
2317
|
+
fs.writeFileSync(outputPath, doc);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
if (docsOutputDir) {
|
|
2321
|
+
const indexContent = `# API Documentation
|
|
2322
|
+
|
|
2323
|
+
Auto-generated documentation for all services.
|
|
2324
|
+
|
|
2325
|
+
## Services
|
|
2326
|
+
|
|
2327
|
+
` + allServices.map((s) => `- [${s.className}](./${s.className}.md)`).join("\n") + `
|
|
2328
|
+
|
|
2329
|
+
---
|
|
2330
|
+
|
|
2331
|
+
*Generated by quickdraw-core*
|
|
2332
|
+
`;
|
|
2333
|
+
fs.writeFileSync(path.join(docsOutputDir, "README.md"), indexContent);
|
|
2334
|
+
}
|
|
2335
|
+
const toolsContent = generateToolsFileContent(allServices);
|
|
2336
|
+
fs.writeFileSync(toolsOutputPath, toolsContent);
|
|
2337
|
+
const totalMethods = allServices.reduce(
|
|
2338
|
+
(sum, s) => sum + s.methods.length,
|
|
2339
|
+
0
|
|
2340
|
+
);
|
|
2341
|
+
console.log(
|
|
2342
|
+
`Generated ${allServices.length} service tools (${totalMethods} methods) -> ${toolsOutputPath}`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
export { BaseService, McpRegistry, ServiceRegistry, applyRateLimitMiddleware, bootstrapMcpServer, checkEnv, createJWT, createMcpRoutes, createMcpStdioServer, createOAuthURL, createQuickdrawServer, createRateLimiter, createTieredRateLimiter, discordProvider, exchangeOAuthCode, generateToolMetadata, getDefaultEntityFields, googleProvider, isRedisAdapterAvailable, mergeWithDefaultFields, requireEnv, setupRedisAdapter, validateEnv, verifyJWT, zodToAdminFields };
|
|
1673
2347
|
//# sourceMappingURL=index.js.map
|
|
1674
2348
|
//# sourceMappingURL=index.js.map
|