Add more mcp tool metadara + ChatGPT domain verification route (#197)
This commit is contained in:
parent
0ce787889a
commit
7a0ab0de2c
@ -25,6 +25,7 @@ import { Route as AppSupportRouteImport } from './routes/_app/support'
|
|||||||
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
|
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
|
||||||
import { Route as AppBillingRouteImport } from './routes/_app/billing'
|
import { Route as AppBillingRouteImport } from './routes/_app/billing'
|
||||||
import { Route as AppAiRouteImport } from './routes/_app/ai'
|
import { Route as AppAiRouteImport } from './routes/_app/ai'
|
||||||
|
import { Route as Char91DotwellKnownChar93OpenaiAppsChallengeRouteImport } from './routes/[.well-known]/openai-apps-challenge'
|
||||||
import { Route as ApiAutumnSplatRouteImport } from './routes/api/autumn/$'
|
import { Route as ApiAutumnSplatRouteImport } from './routes/api/autumn/$'
|
||||||
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
|
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
|
||||||
import { Route as AppHelpDataforseoApiKeyRouteImport } from './routes/_app/help/dataforseo-api-key'
|
import { Route as AppHelpDataforseoApiKeyRouteImport } from './routes/_app/help/dataforseo-api-key'
|
||||||
@ -120,6 +121,12 @@ const AppAiRoute = AppAiRouteImport.update({
|
|||||||
path: '/ai',
|
path: '/ai',
|
||||||
getParentRoute: () => AppRouteRoute,
|
getParentRoute: () => AppRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const Char91DotwellKnownChar93OpenaiAppsChallengeRoute =
|
||||||
|
Char91DotwellKnownChar93OpenaiAppsChallengeRouteImport.update({
|
||||||
|
id: '/.well-known/openai-apps-challenge',
|
||||||
|
path: '/.well-known/openai-apps-challenge',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ApiAutumnSplatRoute = ApiAutumnSplatRouteImport.update({
|
const ApiAutumnSplatRoute = ApiAutumnSplatRouteImport.update({
|
||||||
id: '/api/autumn/$',
|
id: '/api/autumn/$',
|
||||||
path: '/api/autumn/$',
|
path: '/api/autumn/$',
|
||||||
@ -220,6 +227,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/forgot-password': typeof ForgotPasswordRoute
|
'/forgot-password': typeof ForgotPasswordRoute
|
||||||
'/reset-password': typeof ResetPasswordRoute
|
'/reset-password': typeof ResetPasswordRoute
|
||||||
'/verify-email': typeof VerifyEmailRoute
|
'/verify-email': typeof VerifyEmailRoute
|
||||||
|
'/.well-known/openai-apps-challenge': typeof Char91DotwellKnownChar93OpenaiAppsChallengeRoute
|
||||||
'/ai': typeof AppAiRoute
|
'/ai': typeof AppAiRoute
|
||||||
'/billing': typeof AppBillingRoute
|
'/billing': typeof AppBillingRoute
|
||||||
'/settings': typeof AppSettingsRoute
|
'/settings': typeof AppSettingsRoute
|
||||||
@ -251,6 +259,7 @@ export interface FileRoutesByTo {
|
|||||||
'/forgot-password': typeof ForgotPasswordRoute
|
'/forgot-password': typeof ForgotPasswordRoute
|
||||||
'/reset-password': typeof ResetPasswordRoute
|
'/reset-password': typeof ResetPasswordRoute
|
||||||
'/verify-email': typeof VerifyEmailRoute
|
'/verify-email': typeof VerifyEmailRoute
|
||||||
|
'/.well-known/openai-apps-challenge': typeof Char91DotwellKnownChar93OpenaiAppsChallengeRoute
|
||||||
'/ai': typeof AppAiRoute
|
'/ai': typeof AppAiRoute
|
||||||
'/billing': typeof AppBillingRoute
|
'/billing': typeof AppBillingRoute
|
||||||
'/settings': typeof AppSettingsRoute
|
'/settings': typeof AppSettingsRoute
|
||||||
@ -283,6 +292,7 @@ export interface FileRoutesById {
|
|||||||
'/forgot-password': typeof ForgotPasswordRoute
|
'/forgot-password': typeof ForgotPasswordRoute
|
||||||
'/reset-password': typeof ResetPasswordRoute
|
'/reset-password': typeof ResetPasswordRoute
|
||||||
'/verify-email': typeof VerifyEmailRoute
|
'/verify-email': typeof VerifyEmailRoute
|
||||||
|
'/.well-known/openai-apps-challenge': typeof Char91DotwellKnownChar93OpenaiAppsChallengeRoute
|
||||||
'/_app/ai': typeof AppAiRoute
|
'/_app/ai': typeof AppAiRoute
|
||||||
'/_app/billing': typeof AppBillingRoute
|
'/_app/billing': typeof AppBillingRoute
|
||||||
'/_app/settings': typeof AppSettingsRoute
|
'/_app/settings': typeof AppSettingsRoute
|
||||||
@ -317,6 +327,7 @@ export interface FileRouteTypes {
|
|||||||
| '/forgot-password'
|
| '/forgot-password'
|
||||||
| '/reset-password'
|
| '/reset-password'
|
||||||
| '/verify-email'
|
| '/verify-email'
|
||||||
|
| '/.well-known/openai-apps-challenge'
|
||||||
| '/ai'
|
| '/ai'
|
||||||
| '/billing'
|
| '/billing'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
@ -348,6 +359,7 @@ export interface FileRouteTypes {
|
|||||||
| '/forgot-password'
|
| '/forgot-password'
|
||||||
| '/reset-password'
|
| '/reset-password'
|
||||||
| '/verify-email'
|
| '/verify-email'
|
||||||
|
| '/.well-known/openai-apps-challenge'
|
||||||
| '/ai'
|
| '/ai'
|
||||||
| '/billing'
|
| '/billing'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
@ -379,6 +391,7 @@ export interface FileRouteTypes {
|
|||||||
| '/forgot-password'
|
| '/forgot-password'
|
||||||
| '/reset-password'
|
| '/reset-password'
|
||||||
| '/verify-email'
|
| '/verify-email'
|
||||||
|
| '/.well-known/openai-apps-challenge'
|
||||||
| '/_app/ai'
|
| '/_app/ai'
|
||||||
| '/_app/billing'
|
| '/_app/billing'
|
||||||
| '/_app/settings'
|
| '/_app/settings'
|
||||||
@ -415,6 +428,7 @@ export interface RootRouteChildren {
|
|||||||
ForgotPasswordRoute: typeof ForgotPasswordRoute
|
ForgotPasswordRoute: typeof ForgotPasswordRoute
|
||||||
ResetPasswordRoute: typeof ResetPasswordRoute
|
ResetPasswordRoute: typeof ResetPasswordRoute
|
||||||
VerifyEmailRoute: typeof VerifyEmailRoute
|
VerifyEmailRoute: typeof VerifyEmailRoute
|
||||||
|
Char91DotwellKnownChar93OpenaiAppsChallengeRoute: typeof Char91DotwellKnownChar93OpenaiAppsChallengeRoute
|
||||||
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
||||||
ApiAutumnSplatRoute: typeof ApiAutumnSplatRoute
|
ApiAutumnSplatRoute: typeof ApiAutumnSplatRoute
|
||||||
}
|
}
|
||||||
@ -533,6 +547,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppAiRouteImport
|
preLoaderRoute: typeof AppAiRouteImport
|
||||||
parentRoute: typeof AppRouteRoute
|
parentRoute: typeof AppRouteRoute
|
||||||
}
|
}
|
||||||
|
'/.well-known/openai-apps-challenge': {
|
||||||
|
id: '/.well-known/openai-apps-challenge'
|
||||||
|
path: '/.well-known/openai-apps-challenge'
|
||||||
|
fullPath: '/.well-known/openai-apps-challenge'
|
||||||
|
preLoaderRoute: typeof Char91DotwellKnownChar93OpenaiAppsChallengeRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/api/autumn/$': {
|
'/api/autumn/$': {
|
||||||
id: '/api/autumn/$'
|
id: '/api/autumn/$'
|
||||||
path: '/api/autumn/$'
|
path: '/api/autumn/$'
|
||||||
@ -789,6 +810,8 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
ForgotPasswordRoute: ForgotPasswordRoute,
|
ForgotPasswordRoute: ForgotPasswordRoute,
|
||||||
ResetPasswordRoute: ResetPasswordRoute,
|
ResetPasswordRoute: ResetPasswordRoute,
|
||||||
VerifyEmailRoute: VerifyEmailRoute,
|
VerifyEmailRoute: VerifyEmailRoute,
|
||||||
|
Char91DotwellKnownChar93OpenaiAppsChallengeRoute:
|
||||||
|
Char91DotwellKnownChar93OpenaiAppsChallengeRoute,
|
||||||
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
||||||
ApiAutumnSplatRoute: ApiAutumnSplatRoute,
|
ApiAutumnSplatRoute: ApiAutumnSplatRoute,
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/routes/[.well-known]/openai-apps-challenge.ts
Normal file
19
src/routes/[.well-known]/openai-apps-challenge.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
// Public verification token expected at this well-known URL by OpenAI Apps.
|
||||||
|
const OPENAI_APPS_CHALLENGE_TOKEN =
|
||||||
|
"GEqD0QcIISUHCDhQXqm18K9Hm4Fixm8RMbDxz3nUXsw";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/.well-known/openai-apps-challenge")({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: async () => {
|
||||||
|
return new Response(OPENAI_APPS_CHALLENGE_TOKEN, {
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/plain; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
18
src/server/mcp/output-schemas.ts
Normal file
18
src/server/mcp/output-schemas.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const mcpMetaOutputSchema = z
|
||||||
|
.object({
|
||||||
|
url: z.string().optional(),
|
||||||
|
organizationId: z.string().optional(),
|
||||||
|
projectId: z.string().optional(),
|
||||||
|
runId: z.string().optional(),
|
||||||
|
creditsCharged: z.number().optional(),
|
||||||
|
creditsRemaining: z.number().optional(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
export const looseObjectOutputSchema = z.record(z.string(), z.unknown());
|
||||||
|
|
||||||
|
export const optionalMetaOutputSchema = {
|
||||||
|
meta: mcpMetaOutputSchema.optional(),
|
||||||
|
} as const;
|
||||||
@ -2,6 +2,10 @@ import { z } from "zod";
|
|||||||
import { BacklinksService } from "@/server/features/backlinks/services/BacklinksService";
|
import { BacklinksService } from "@/server/features/backlinks/services/BacklinksService";
|
||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { buildProjectMeta } from "@/server/mcp/context";
|
import { buildProjectMeta } from "@/server/mcp/context";
|
||||||
|
import {
|
||||||
|
looseObjectOutputSchema,
|
||||||
|
optionalMetaOutputSchema,
|
||||||
|
} from "@/server/mcp/output-schemas";
|
||||||
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
||||||
import { projectIdSchema } from "@/server/mcp/schemas";
|
import { projectIdSchema } from "@/server/mcp/schemas";
|
||||||
|
|
||||||
@ -38,6 +42,16 @@ export const getBacklinksOverviewTool = {
|
|||||||
description:
|
description:
|
||||||
"Returns a backlinks profile summary (total backlinks, referring domains, top referring domains). Charges credits (~200-500 typical). Requires that the user's DataForSEO account has Backlinks enabled.",
|
"Returns a backlinks profile summary (total backlinks, referring domains, top referring domains). Charges credits (~200-500 typical). Requires that the user's DataForSEO account has Backlinks enabled.",
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
outputSchema: {
|
||||||
|
overview: looseObjectOutputSchema,
|
||||||
|
referringDomains: looseObjectOutputSchema,
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: withMcpProjectAuth(async (args: Args, context) => {
|
handler: withMcpProjectAuth(async (args: Args, context) => {
|
||||||
const lookup = { target: args.target, scope: args.scope };
|
const lookup = { target: args.target, scope: args.scope };
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { z } from "zod";
|
|||||||
import { DomainService } from "@/server/features/domain/services/DomainService";
|
import { DomainService } from "@/server/features/domain/services/DomainService";
|
||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { buildProjectMeta } from "@/server/mcp/context";
|
import { buildProjectMeta } from "@/server/mcp/context";
|
||||||
|
import {
|
||||||
|
looseObjectOutputSchema,
|
||||||
|
optionalMetaOutputSchema,
|
||||||
|
} from "@/server/mcp/output-schemas";
|
||||||
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANGUAGE_CODE,
|
DEFAULT_LANGUAGE_CODE,
|
||||||
@ -30,6 +34,15 @@ export const getDomainKeywordSuggestionsTool = {
|
|||||||
description:
|
description:
|
||||||
"Returns the organic keywords a domain ranks for, including position and available metrics. Use after get_domain_overview when you want the detailed keyword opportunity list for a competitor or reference domain. Charges credits (~100-300 typical). Cached for 12 hours.",
|
"Returns the organic keywords a domain ranks for, including position and available metrics. Use after get_domain_overview when you want the detailed keyword opportunity list for a competitor or reference domain. Charges credits (~100-300 typical). Cached for 12 hours.",
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
outputSchema: {
|
||||||
|
keywords: z.array(looseObjectOutputSchema),
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: withMcpProjectAuth(async (args: Args, context) => {
|
handler: withMcpProjectAuth(async (args: Args, context) => {
|
||||||
const keywords = await DomainService.getSuggestedKeywords(
|
const keywords = await DomainService.getSuggestedKeywords(
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
import { DomainService } from "@/server/features/domain/services/DomainService";
|
import { DomainService } from "@/server/features/domain/services/DomainService";
|
||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { buildProjectMeta } from "@/server/mcp/context";
|
import { buildProjectMeta } from "@/server/mcp/context";
|
||||||
|
import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas";
|
||||||
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANGUAGE_CODE,
|
DEFAULT_LANGUAGE_CODE,
|
||||||
@ -28,6 +29,21 @@ export const getDomainOverviewTool = {
|
|||||||
description:
|
description:
|
||||||
"Returns a high-level view of a domain's organic footprint: estimated organic traffic, organic keyword count, backlinks, and referring domains. Use this first for domain research; for the detailed ranked-keyword list, call get_domain_keyword_suggestions next. Charges credits (~100-300 typical). Cached for 12 hours per domain.",
|
"Returns a high-level view of a domain's organic footprint: estimated organic traffic, organic keyword count, backlinks, and referring domains. Use this first for domain research; for the detailed ranked-keyword list, call get_domain_keyword_suggestions next. Charges credits (~100-300 typical). Cached for 12 hours per domain.",
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
outputSchema: z
|
||||||
|
.object({
|
||||||
|
domain: z.string().optional(),
|
||||||
|
organicTraffic: z.number().nullable().optional(),
|
||||||
|
organicKeywords: z.number().nullable().optional(),
|
||||||
|
backlinks: z.number().nullable().optional(),
|
||||||
|
referringDomains: z.number().nullable().optional(),
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: withMcpProjectAuth(async (args: Args, context) => {
|
handler: withMcpProjectAuth(async (args: Args, context) => {
|
||||||
const result = await DomainService.getOverview(
|
const result = await DomainService.getOverview(
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import { RankTrackingRepository } from "@/server/features/rank-tracking/reposito
|
|||||||
import { getLatestResults } from "@/server/features/rank-tracking/services/rankTrackingResults";
|
import { getLatestResults } from "@/server/features/rank-tracking/services/rankTrackingResults";
|
||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { buildProjectMeta } from "@/server/mcp/context";
|
import { buildProjectMeta } from "@/server/mcp/context";
|
||||||
|
import {
|
||||||
|
looseObjectOutputSchema,
|
||||||
|
optionalMetaOutputSchema,
|
||||||
|
} from "@/server/mcp/output-schemas";
|
||||||
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
||||||
import { projectIdSchema } from "@/server/mcp/schemas";
|
import { projectIdSchema } from "@/server/mcp/schemas";
|
||||||
|
|
||||||
@ -25,6 +29,19 @@ export const getRankTrackerTool = {
|
|||||||
description:
|
description:
|
||||||
"Read-only access to rank tracker configs and their latest results. With `trackerId`, returns config + latest snapshot per keyword. Without it, lists all trackers in the project. Free — reads from OpenSEO state, no DataForSEO call. To trigger a new check, use the dashboard.",
|
"Read-only access to rank tracker configs and their latest results. With `trackerId`, returns config + latest snapshot per keyword. Without it, lists all trackers in the project. Free — reads from OpenSEO state, no DataForSEO call. To trigger a new check, use the dashboard.",
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
outputSchema: z
|
||||||
|
.object({
|
||||||
|
configs: z.array(looseObjectOutputSchema).optional(),
|
||||||
|
config: looseObjectOutputSchema.optional(),
|
||||||
|
results: looseObjectOutputSchema.optional(),
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: withMcpProjectAuth(async (args: Args, context) => {
|
handler: withMcpProjectAuth(async (args: Args, context) => {
|
||||||
if (!args.trackerId) {
|
if (!args.trackerId) {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
import { createDataforseoClient } from "@/server/lib/dataforseoClient";
|
import { createDataforseoClient } from "@/server/lib/dataforseoClient";
|
||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { buildProjectMeta } from "@/server/mcp/context";
|
import { buildProjectMeta } from "@/server/mcp/context";
|
||||||
|
import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas";
|
||||||
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANGUAGE_CODE,
|
DEFAULT_LANGUAGE_CODE,
|
||||||
@ -37,6 +38,43 @@ export const getSerpResultsTool = {
|
|||||||
description:
|
description:
|
||||||
"Fetch live Google organic search results for 1-10 keywords. Use this to inspect who ranks for a query, verify competitors, compare SERPs across keywords, or gather source URLs before content planning. Charges credits per keyword (~30-60 each). Does not save results to OpenSEO. Per-keyword errors don't fail the batch.",
|
"Fetch live Google organic search results for 1-10 keywords. Use this to inspect who ranks for a query, verify competitors, compare SERPs across keywords, or gather source URLs before content planning. Charges credits per keyword (~30-60 each). Does not save results to OpenSEO. Per-keyword errors don't fail the batch.",
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
outputSchema: {
|
||||||
|
results: z.array(
|
||||||
|
z.union([
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
keyword: z.string(),
|
||||||
|
ok: z.literal(true),
|
||||||
|
items: z.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.string().nullable().optional(),
|
||||||
|
rank: z.number().nullable(),
|
||||||
|
title: z.string().nullable(),
|
||||||
|
url: z.string().nullable(),
|
||||||
|
domain: z.string().nullable(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
keyword: z.string(),
|
||||||
|
ok: z.literal(false),
|
||||||
|
error: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: withMcpProjectAuth(async (args: Args, context) => {
|
handler: withMcpProjectAuth(async (args: Args, context) => {
|
||||||
const client = createDataforseoClient(context.billing);
|
const client = createDataforseoClient(context.billing);
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { ProjectService } from "@/server/features/projects/services/ProjectService";
|
import { ProjectService } from "@/server/features/projects/services/ProjectService";
|
||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { getAuth, getBaseUrl, type ToolExtra } from "@/server/mcp/context";
|
import { getAuth, getBaseUrl, type ToolExtra } from "@/server/mcp/context";
|
||||||
|
import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas";
|
||||||
import { buildDashboardUrl } from "@/server/mcp/urls";
|
import { buildDashboardUrl } from "@/server/mcp/urls";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export const listProjectsTool = {
|
export const listProjectsTool = {
|
||||||
name: "list_projects",
|
name: "list_projects",
|
||||||
@ -10,6 +12,24 @@ export const listProjectsTool = {
|
|||||||
description:
|
description:
|
||||||
"Lists all projects in the user's organization. Free — does not call DataForSEO. Use this whenever you need a `projectId` for another OpenSEO tool. Returns an array of {id, name, domain}; pass the `id` value as `projectId`.",
|
"Lists all projects in the user's organization. Free — does not call DataForSEO. Use this whenever you need a `projectId` for another OpenSEO tool. Returns an array of {id, name, domain}; pass the `id` value as `projectId`.",
|
||||||
inputSchema: {} as Record<string, never>,
|
inputSchema: {} as Record<string, never>,
|
||||||
|
outputSchema: {
|
||||||
|
projects: z.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
domain: z.string().nullable().optional(),
|
||||||
|
url: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
),
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: async (_args: Record<string, never>, extra: ToolExtra) => {
|
handler: async (_args: Record<string, never>, extra: ToolExtra) => {
|
||||||
const auth = getAuth(extra);
|
const auth = getAuth(extra);
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { z } from "zod";
|
|||||||
import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService";
|
import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService";
|
||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { buildProjectMeta } from "@/server/mcp/context";
|
import { buildProjectMeta } from "@/server/mcp/context";
|
||||||
|
import {
|
||||||
|
looseObjectOutputSchema,
|
||||||
|
optionalMetaOutputSchema,
|
||||||
|
} from "@/server/mcp/output-schemas";
|
||||||
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
||||||
import { projectIdSchema } from "@/server/mcp/schemas";
|
import { projectIdSchema } from "@/server/mcp/schemas";
|
||||||
|
|
||||||
@ -31,6 +35,17 @@ export const listSavedKeywordsTool = {
|
|||||||
description:
|
description:
|
||||||
"Lists keywords saved to a project (with cached metrics like search volume, difficulty, CPC, and tags if available). Free — reads from OpenSEO's database, no DataForSEO call. Use tag filters when the user asks for a saved segment; multiple tags match ANY tag.",
|
"Lists keywords saved to a project (with cached metrics like search volume, difficulty, CPC, and tags if available). Free — reads from OpenSEO's database, no DataForSEO call. Use tag filters when the user asks for a saved segment; multiple tags match ANY tag.",
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
outputSchema: {
|
||||||
|
rows: z.array(looseObjectOutputSchema),
|
||||||
|
totalCount: z.number(),
|
||||||
|
tags: z.array(looseObjectOutputSchema),
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: withMcpProjectAuth(
|
handler: withMcpProjectAuth(
|
||||||
async (args: z.infer<z.ZodObject<typeof inputSchema>>, context) => {
|
async (args: z.infer<z.ZodObject<typeof inputSchema>>, context) => {
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { z } from "zod";
|
|||||||
import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService";
|
import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService";
|
||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { buildProjectMeta } from "@/server/mcp/context";
|
import { buildProjectMeta } from "@/server/mcp/context";
|
||||||
|
import {
|
||||||
|
looseObjectOutputSchema,
|
||||||
|
optionalMetaOutputSchema,
|
||||||
|
} from "@/server/mcp/output-schemas";
|
||||||
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANGUAGE_CODE,
|
DEFAULT_LANGUAGE_CODE,
|
||||||
@ -41,6 +45,35 @@ export const researchKeywordsTool = {
|
|||||||
description:
|
description:
|
||||||
"Research keyword data (search volume, difficulty, CPC, related ideas) for 1-5 seed keywords in one call. Charges credits per seed (~50-200 credits each, varies by source). Returns per-seed results — a single bad seed won't fail the batch.",
|
"Research keyword data (search volume, difficulty, CPC, related ideas) for 1-5 seed keywords in one call. Charges credits per seed (~50-200 credits each, varies by source). Returns per-seed results — a single bad seed won't fail the batch.",
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
outputSchema: {
|
||||||
|
results: z.array(
|
||||||
|
z.union([
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
seed: z.string(),
|
||||||
|
ok: z.literal(true),
|
||||||
|
rowCount: z.number(),
|
||||||
|
source: z.string(),
|
||||||
|
usedFallback: z.boolean(),
|
||||||
|
topRows: z.array(looseObjectOutputSchema),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
seed: z.string(),
|
||||||
|
ok: z.literal(false),
|
||||||
|
error: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: withMcpProjectAuth(async (args: Args, context) => {
|
handler: withMcpProjectAuth(async (args: Args, context) => {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService";
|
import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService";
|
||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { buildProjectMeta } from "@/server/mcp/context";
|
import { buildProjectMeta } from "@/server/mcp/context";
|
||||||
|
import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas";
|
||||||
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANGUAGE_CODE,
|
DEFAULT_LANGUAGE_CODE,
|
||||||
@ -44,6 +45,21 @@ export const saveKeywordsTool = {
|
|||||||
description:
|
description:
|
||||||
"Save keywords to a project's saved-keywords list. Free — does not call DataForSEO. Idempotent: re-saving an existing keyword is a no-op. If tags are provided, missing tags may be created. By default tags are appended; set tagMode=replace to remove existing tags from these saved keywords before applying the provided tags, which is useful for reorganizing keywords into page/topic clusters. Ask the user for confirmation before applying or replacing tags broadly.",
|
"Save keywords to a project's saved-keywords list. Free — does not call DataForSEO. Idempotent: re-saving an existing keyword is a no-op. If tags are provided, missing tags may be created. By default tags are appended; set tagMode=replace to remove existing tags from these saved keywords before applying the provided tags, which is useful for reorganizing keywords into page/topic clusters. Ask the user for confirmation before applying or replacing tags broadly.",
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
outputSchema: {
|
||||||
|
projectId: z.string(),
|
||||||
|
savedCount: z.number(),
|
||||||
|
keywords: z.array(z.string()),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
tagMode: z.enum(["append", "replace"]),
|
||||||
|
locationCode: z.number(),
|
||||||
|
languageCode: z.string(),
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: withMcpProjectAuth(async (args: Args, context) => {
|
handler: withMcpProjectAuth(async (args: Args, context) => {
|
||||||
if (args.tagMode === "replace" && (args.tags?.length ?? 0) === 0) {
|
if (args.tagMode === "replace" && (args.tags?.length ?? 0) === 0) {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import {
|
|||||||
import { mcpResponse } from "@/server/mcp/formatters";
|
import { mcpResponse } from "@/server/mcp/formatters";
|
||||||
import { getAuth, type ToolExtra } from "@/server/mcp/context";
|
import { getAuth, type ToolExtra } from "@/server/mcp/context";
|
||||||
import { isHostedServerAuthMode } from "@/server/lib/runtime-env";
|
import { isHostedServerAuthMode } from "@/server/lib/runtime-env";
|
||||||
|
import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
async function checkBalance(featureId: string, customerId: string) {
|
async function checkBalance(featureId: string, customerId: string) {
|
||||||
try {
|
try {
|
||||||
@ -23,6 +25,20 @@ export const whoamiTool = {
|
|||||||
description:
|
description:
|
||||||
"Returns the authenticated user, organization, server mode, token scopes, and current credit balance. Free — does not call DataForSEO. Use this first to confirm connection context before choosing a project or running paid tools.",
|
"Returns the authenticated user, organization, server mode, token scopes, and current credit balance. Free — does not call DataForSEO. Use this first to confirm connection context before choosing a project or running paid tools.",
|
||||||
inputSchema: {} as Record<string, never>,
|
inputSchema: {} as Record<string, never>,
|
||||||
|
outputSchema: {
|
||||||
|
userId: z.string(),
|
||||||
|
userEmail: z.string(),
|
||||||
|
organizationId: z.string(),
|
||||||
|
scopes: z.array(z.string()),
|
||||||
|
mode: z.enum(["hosted", "self-hosted"]),
|
||||||
|
creditsRemaining: z.number().nullable(),
|
||||||
|
...optionalMetaOutputSchema,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
openWorldHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handler: async (_args: Record<string, never>, extra: ToolExtra) => {
|
handler: async (_args: Record<string, never>, extra: ToolExtra) => {
|
||||||
const auth = getAuth(extra);
|
const auth = getAuth(extra);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user