Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Mark auto-generated files so GitHub linguist ignores them in code stats
src/mcp/tools/installGuides.generated.ts linguist-generated
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ oclif.manifest.json
README.md
**/*.snap
test-utils/fixtures

# Generated install guides enum
src/mcp/tools/installGuides.generated.ts
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ $ npm install -g @devcycle/cli
$ dvc COMMAND
running command...
$ dvc (--version)
@devcycle/cli/6.0.1 linux-x64 node-v22.18.0
@devcycle/cli/6.0.1 darwin-arm64 node-v22.17.1
$ dvc --help [COMMAND]
USAGE
$ dvc COMMAND
Expand Down
2 changes: 1 addition & 1 deletion oclif.manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "6.0.0",
"version": "6.0.1",
"commands": {
"authCommand": {
"id": "authCommand",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"/oclif.manifest.json"
],
"scripts": {
"build": "shx rm -rf dist && tsc -b && oclif manifest",
"build": "node scripts/fetch-install-prompts.js && shx rm -rf dist && tsc -b && oclif manifest",
"build:tar": "oclif pack tarballs",
"build:worker": "cd mcp-worker && yarn build",
"deploy:worker": "cd mcp-worker && yarn deploy",
Expand Down
127 changes: 127 additions & 0 deletions scripts/fetch-install-prompts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env node
/*
* Auto-generates src/mcp/tools/installGuides.generated.ts
* by listing all Markdown files under install-prompts/ (recursively)
* from the AI-Prompts-And-Rules repo on the main branch.
*
* Includes OpenFeature guides automatically.
*/

const https = require('https')
const fs = require('fs')
const path = require('path')

// Keep a single constant for clarity and reuse
const TREE_URL = 'https://api.github.com/repos/DevCycleHQ/AI-Prompts-And-Rules/git/trees/main?recursive=1'

function fetchJson(url, headers = {}) {
const requestHeaders = {
'User-Agent': 'devcycle-cli-build-script',
Accept: 'application/vnd.github+json',
...headers,
}
return new Promise((resolve, reject) => {
const req = https.get(url, { headers: requestHeaders }, (res) => {
let data = ''
res.on('data', (chunk) => (data += chunk))
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(data))
} catch (err) {
reject(err)
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`))
}
})
})
req.on('error', reject)
req.end()
})
}

async function main() {
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN
const headers = token ? { Authorization: `Bearer ${token}` } : {}

const outFile = path.resolve(
__dirname,
'..',
'src',
'mcp',
'tools',
'installGuides.generated.ts',
)

try {
const tree = await fetchJson(TREE_URL, headers)
const all = Array.isArray(tree.tree) ? tree.tree : []

// Validate and sanitize file paths returned by GitHub API
const isValidPath = (filePath) => {
return (
typeof filePath === 'string' &&
filePath.length > 0 &&
!filePath.includes('..') &&
!path.isAbsolute(filePath) &&
filePath.startsWith('install-prompts/') &&
/^[a-zA-Z0-9_.\-\/]+$/.test(filePath)
)
}

const mdFiles = all
.filter((item) => item.type === 'blob')
.map((item) => item.path)
.filter(isValidPath)
.filter((p) => p.toLowerCase().endsWith('.md'))

// Build safe slugs (relative paths within install-prompts without extension)
const slugSet = new Set()
for (const p of mdFiles) {
const raw = p.replace(/^install-prompts\//, '').replace(/\.md$/i, '')
// extra guards on the slug
if (!raw || raw.includes('..') || raw.startsWith('/')) continue
const cleaned = raw
// allow only safe characters (letters, numbers, dash, underscore, slash)
.replace(/[^a-zA-Z0-9_\-\/]/g, '')
// collapse multiple slashes
.replace(/\/+\/+/g, '/')
// trim leading/trailing slashes
.replace(/^\/+|\/+$/g, '')
if (cleaned) slugSet.add(cleaned)
}

const slugs = Array.from(slugSet).sort((a, b) => a.localeCompare(b))

const content = `// AUTO-GENERATED BY scripts/fetch-install-prompts.js. DO NOT EDIT.
export const INSTALL_GUIDES = ${JSON.stringify(slugs, null, 2)} as const
export type InstallGuideId = typeof INSTALL_GUIDES[number]
`

fs.writeFileSync(outFile, content, 'utf8')
console.log(
`Generated ${outFile} with ${slugs.length} install guide entries.`,
)
} catch (err) {
const message = `[fetch-install-prompts] Failed to generate guides list: ${
err && err.message ? err.message : err
}`
if (fs.existsSync(outFile)) {
console.warn(
`${message}. Existing generated file found at ${outFile}. Proceeding with previously generated data.`,
)
return
}
// No previously generated file; fail immediately.
throw new Error(
`${message}. No existing generated file found. Cannot proceed.`,
)
}
}

main().catch((err) => {
console.error(err)
process.exit(1)
})

12 changes: 12 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ export class DevCycleMCPServer {
try {
const result = await handler(args)

// If the handler returned a plain string, send it as-is
if (typeof result === 'string') {
return {
content: [
{
type: 'text' as const,
text: result,
},
],
}
}

return {
content: [
{
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { registerSelfTargetingTools } from './selfTargetingTools'
import { registerVariableTools } from './variableTools'
import { registerLocalProjectTools } from './localProjectTools'
import { DevCycleApiClient } from '../utils/api'
import { registerInstallTools } from './installTools'

/**
* Register all DevCycle MCP tools with a server instance
Expand All @@ -26,6 +27,7 @@ export function registerAllToolsWithServer(
registerResultsTools(serverInstance, apiClient)
registerSelfTargetingTools(serverInstance, apiClient)
registerVariableTools(serverInstance, apiClient)
registerInstallTools(serverInstance)

// Register local project selection tools only for local MCP (not worker)
// We detect local MCP by checking if the apiClient is an instance of DevCycleApiClient
Expand Down
33 changes: 33 additions & 0 deletions src/mcp/tools/installGuides.generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions src/mcp/tools/installTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import axios from 'axios'
import { z } from 'zod'
import type { IDevCycleApiClient } from '../api/interface'
import type { DevCycleMCPServerInstance } from '../server'
import { INSTALL_GUIDES } from './installGuides.generated'

const InstallGuideArgsSchema = z.object({
guide: z.enum(INSTALL_GUIDES),
})

type InstallGuideArgs = z.infer<typeof InstallGuideArgsSchema>

async function fetchInstallGuideHandler(args: InstallGuideArgs) {
const trimmedGuide = args.guide.trim().replace(/^\/+|\/+$/g, '')
const fileName = trimmedGuide.endsWith('.md')
? trimmedGuide
: `${trimmedGuide}.md`
const repoPath = `install-prompts/${fileName}`
const sourceUrl = `https://raw.githubusercontent.com/DevCycleHQ/AI-Prompts-And-Rules/main/${repoPath}`

try {
const response = await axios.get<string>(sourceUrl, {
responseType: 'text',
})
return response.data as string
} catch (error: unknown) {
const status = axios.isAxiosError(error)
? error.response?.status
: undefined
if (status === 404) {
throw new Error(
`Install guide "${fileName}" not found in install-prompts/. Check the filename (with or without .md).`,
)
}
throw new Error(
'Unable to fetch install guide from GitHub. Please retry.',
)
}
}

export function registerInstallTools(
serverInstance: DevCycleMCPServerInstance,
): void {
serverInstance.registerToolWithErrorHandling(
'install_devcycle_sdk',
{
description: [
'Fetch DevCycle SDK installation instructions, and follow the instructions to install the DevCycle SDK.',
"Choose the guide that matches the application's language/framework.",
].join('\n'),
annotations: {
title: 'Install DevCycle SDK',
readOnlyHint: true,
},
inputSchema: InstallGuideArgsSchema.shape,
},
async (args: unknown) => {
const validatedArgs = InstallGuideArgsSchema.parse(args)
return await fetchInstallGuideHandler(validatedArgs)
},
)
}