Add landing page, referral system, usage quotas, search API management, and yearly pricing

- Separate workspace landing from login for better UX
- Referral system rewards both parties with Pro days
- Quota enforcement prevents abuse without breaking endpoints
- 7-day free trial with auto-downgrade on expiry
- Admin-managed search provider config (SearXNG, Bing)
- 15% discount on annual subscriptions
- MCP search server wrapping opencode search
- Fix discovery module field name mismatch causing 422
This commit is contained in:
TradeMate Dev
2026-05-26 11:40:13 +08:00
parent 52dba37f22
commit bed5c7abef
39 changed files with 1988 additions and 152 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
dist/
+20
View File
@@ -0,0 +1,20 @@
{
"name": "opencode-search-mcp",
"version": "1.0.0",
"description": "MCP server wrapping opencode search capabilities",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"@opencode-ai/sdk": "^1.14.41",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
}
+139
View File
@@ -0,0 +1,139 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createOpencodeClient } from "@opencode-ai/sdk";
import { z } from "zod";
const server = new McpServer({
name: "opencode-search",
version: "1.0.0",
});
const client = createOpencodeClient({
baseUrl: process.env.OPENCODE_URL || "http://127.0.0.1:4096",
});
server.registerTool(
"search_files",
{
title: "Search Files",
description: "Search for files and directories by name in the opencode workspace",
inputSchema: z.object({
query: z.string(),
directory: z.string().optional(),
limit: z.number().min(1).max(200).optional(),
}),
},
async ({ query, directory, limit }) => {
try {
const results = await (client.find.files as any)({
query: {
query,
directory: directory || undefined,
limit: limit || 50,
},
});
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
} catch (e) {
return {
content: [{ type: "text", text: `Error: ${(e as Error).message}` }],
isError: true,
};
}
}
);
server.registerTool(
"search_text",
{
title: "Search Text",
description: "Search for text content within files using regex patterns",
inputSchema: z.object({
pattern: z.string(),
directory: z.string().optional(),
}),
},
async ({ pattern, directory }) => {
try {
const results = await (client.find.text as any)({
query: {
pattern,
directory: directory || undefined,
},
});
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
} catch (e) {
return {
content: [{ type: "text", text: `Error: ${(e as Error).message}` }],
isError: true,
};
}
}
);
server.registerTool(
"search_symbols",
{
title: "Search Symbols",
description: "Search for code symbols in the workspace",
inputSchema: z.object({
pattern: z.string(),
directory: z.string().optional(),
limit: z.number().min(1).max(200).optional(),
}),
},
async ({ pattern, directory, limit }) => {
try {
const results = await (client.find.symbols as any)({
query: {
pattern,
directory: directory || undefined,
limit: limit || 50,
},
});
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
} catch (e) {
return {
content: [{ type: "text", text: `Error: ${(e as Error).message}` }],
isError: true,
};
}
}
);
server.registerTool(
"get_workspace_path",
{
title: "Get Workspace Path",
description: "Get the current opencode workspace path info",
inputSchema: z.object({}),
},
async () => {
try {
const path = await client.path.get();
return {
content: [{ type: "text", text: JSON.stringify(path, null, 2) }],
};
} catch (e) {
return {
content: [{ type: "text", text: `Error: ${(e as Error).message}` }],
isError: true,
};
}
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((e) => {
console.error("MCP server error:", e);
process.exit(1);
});
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}