first commit
This commit is contained in:
26
src/cli/package.json
Normal file
26
src/cli/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@mcpctl/cli",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcpctl": "./dist/index.js"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "tsx src/index.ts",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^13.0.0",
|
||||
"chalk": "^5.4.0",
|
||||
"inquirer": "^12.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"@mcpctl/shared": "workspace:*",
|
||||
"@mcpctl/db": "workspace:*"
|
||||
}
|
||||
}
|
||||
2
src/cli/src/index.ts
Normal file
2
src/cli/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// mcpctl CLI entry point
|
||||
// Will be implemented in Task 7
|
||||
9
src/cli/src/registry/base.ts
Normal file
9
src/cli/src/registry/base.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { RegistryServer } from './types.js';
|
||||
|
||||
export abstract class RegistrySource {
|
||||
abstract readonly name: string;
|
||||
|
||||
abstract search(query: string, limit: number): Promise<RegistryServer[]>;
|
||||
|
||||
protected abstract normalizeResult(raw: unknown): RegistryServer;
|
||||
}
|
||||
57
src/cli/src/registry/cache.ts
Normal file
57
src/cli/src/registry/cache.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createHash } from 'crypto';
|
||||
import type { RegistryServer, SearchOptions } from './types.js';
|
||||
|
||||
export class RegistryCache {
|
||||
private cache = new Map<string, { data: RegistryServer[]; expires: number }>();
|
||||
private defaultTTL: number;
|
||||
private hits = 0;
|
||||
private misses = 0;
|
||||
|
||||
constructor(ttlMs = 3_600_000) {
|
||||
this.defaultTTL = ttlMs;
|
||||
}
|
||||
|
||||
private getKey(query: string, options: SearchOptions): string {
|
||||
return createHash('sha256')
|
||||
.update(JSON.stringify({ query, options }))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
get(query: string, options: SearchOptions): RegistryServer[] | null {
|
||||
const key = this.getKey(query, options);
|
||||
const entry = this.cache.get(key);
|
||||
if (entry !== undefined && entry.expires > Date.now()) {
|
||||
this.hits++;
|
||||
return entry.data;
|
||||
}
|
||||
if (entry !== undefined) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
this.misses++;
|
||||
return null;
|
||||
}
|
||||
|
||||
set(query: string, options: SearchOptions, data: RegistryServer[]): void {
|
||||
const key = this.getKey(query, options);
|
||||
this.cache.set(key, { data, expires: Date.now() + this.defaultTTL });
|
||||
}
|
||||
|
||||
getHitRatio(): { hits: number; misses: number; ratio: number } {
|
||||
const total = this.hits + this.misses;
|
||||
return {
|
||||
hits: this.hits,
|
||||
misses: this.misses,
|
||||
ratio: total === 0 ? 0 : this.hits / total,
|
||||
};
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
this.hits = 0;
|
||||
this.misses = 0;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
}
|
||||
76
src/cli/src/registry/dedup.ts
Normal file
76
src/cli/src/registry/dedup.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { RegistryServer, EnvVar } from './types.js';
|
||||
|
||||
function normalizeGitHubUrl(url: string): string {
|
||||
return url
|
||||
.replace(/^git@github\.com:/, 'https://github.com/')
|
||||
.replace(/\.git$/, '')
|
||||
.replace(/\/$/, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function mergeEnvTemplates(a: EnvVar[], b: EnvVar[]): EnvVar[] {
|
||||
const seen = new Map<string, EnvVar>();
|
||||
for (const v of a) {
|
||||
seen.set(v.name, v);
|
||||
}
|
||||
for (const v of b) {
|
||||
if (!seen.has(v.name)) {
|
||||
seen.set(v.name, v);
|
||||
}
|
||||
}
|
||||
return [...seen.values()];
|
||||
}
|
||||
|
||||
export function deduplicateResults(results: RegistryServer[]): RegistryServer[] {
|
||||
const byNpm = new Map<string, RegistryServer>();
|
||||
const byRepo = new Map<string, RegistryServer>();
|
||||
const deduped: RegistryServer[] = [];
|
||||
|
||||
for (const server of results) {
|
||||
const npmKey = server.packages.npm;
|
||||
const repoKey =
|
||||
server.repositoryUrl !== undefined ? normalizeGitHubUrl(server.repositoryUrl) : undefined;
|
||||
|
||||
let existing: RegistryServer | undefined;
|
||||
|
||||
if (npmKey !== undefined) {
|
||||
existing = byNpm.get(npmKey);
|
||||
}
|
||||
if (existing === undefined && repoKey !== undefined) {
|
||||
existing = byRepo.get(repoKey);
|
||||
}
|
||||
|
||||
if (existing !== undefined) {
|
||||
// Merge: keep higher popularity, combine envTemplates
|
||||
if (server.popularityScore > existing.popularityScore) {
|
||||
const merged: RegistryServer = {
|
||||
...server,
|
||||
envTemplate: mergeEnvTemplates(server.envTemplate, existing.envTemplate),
|
||||
verified: server.verified || existing.verified,
|
||||
};
|
||||
// Replace existing in deduped array
|
||||
const idx = deduped.indexOf(existing);
|
||||
if (idx !== -1) {
|
||||
deduped[idx] = merged;
|
||||
}
|
||||
// Update maps to point to merged
|
||||
if (npmKey !== undefined) byNpm.set(npmKey, merged);
|
||||
if (repoKey !== undefined) byRepo.set(repoKey, merged);
|
||||
if (existing.packages.npm !== undefined) byNpm.set(existing.packages.npm, merged);
|
||||
if (existing.repositoryUrl !== undefined) {
|
||||
byRepo.set(normalizeGitHubUrl(existing.repositoryUrl), merged);
|
||||
}
|
||||
} else {
|
||||
// Keep existing but merge envTemplates
|
||||
existing.envTemplate = mergeEnvTemplates(existing.envTemplate, server.envTemplate);
|
||||
existing.verified = existing.verified || server.verified;
|
||||
}
|
||||
} else {
|
||||
deduped.push(server);
|
||||
if (npmKey !== undefined) byNpm.set(npmKey, server);
|
||||
if (repoKey !== undefined) byRepo.set(repoKey, server);
|
||||
}
|
||||
}
|
||||
|
||||
return deduped;
|
||||
}
|
||||
180
src/cli/src/registry/types.ts
Normal file
180
src/cli/src/registry/types.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// ── Normalized types used throughout mcpctl ──
|
||||
|
||||
export interface EnvVar {
|
||||
name: string;
|
||||
description: string;
|
||||
isSecret: boolean;
|
||||
setupUrl?: string;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export interface RegistryServer {
|
||||
name: string;
|
||||
description: string;
|
||||
packages: {
|
||||
npm?: string;
|
||||
pypi?: string;
|
||||
docker?: string;
|
||||
};
|
||||
envTemplate: EnvVar[];
|
||||
transport: 'stdio' | 'sse' | 'streamable-http';
|
||||
repositoryUrl?: string;
|
||||
popularityScore: number;
|
||||
verified: boolean;
|
||||
sourceRegistry: 'official' | 'glama' | 'smithery';
|
||||
lastUpdated?: Date;
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
query: string;
|
||||
limit?: number;
|
||||
registries?: RegistryName[];
|
||||
verified?: boolean;
|
||||
transport?: 'stdio' | 'sse';
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export type RegistryName = 'official' | 'glama' | 'smithery';
|
||||
|
||||
export interface RegistryClientConfig {
|
||||
registries?: RegistryName[];
|
||||
cacheTTLMs?: number;
|
||||
smitheryApiKey?: string;
|
||||
httpProxy?: string;
|
||||
httpsProxy?: string;
|
||||
}
|
||||
|
||||
// ── Zod schemas for API response validation ──
|
||||
|
||||
// Official MCP Registry
|
||||
const OfficialEnvVarSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional().default(''),
|
||||
format: z.string().optional(),
|
||||
isSecret: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
const OfficialPackageSchema = z.object({
|
||||
registryType: z.string(),
|
||||
identifier: z.string(),
|
||||
version: z.string().optional(),
|
||||
runtimeHint: z.string().optional(),
|
||||
transport: z.object({
|
||||
type: z.string(),
|
||||
}).optional(),
|
||||
environmentVariables: z.array(OfficialEnvVarSchema).optional().default([]),
|
||||
});
|
||||
|
||||
const OfficialRemoteSchema = z.object({
|
||||
type: z.string(),
|
||||
url: z.string(),
|
||||
headers: z.array(z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional().default(''),
|
||||
value: z.string().optional(),
|
||||
isRequired: z.boolean().optional(),
|
||||
isSecret: z.boolean().optional(),
|
||||
})).optional().default([]),
|
||||
});
|
||||
|
||||
const OfficialServerSchema = z.object({
|
||||
server: z.object({
|
||||
name: z.string(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional().default(''),
|
||||
version: z.string().optional(),
|
||||
repository: z.object({
|
||||
url: z.string(),
|
||||
source: z.string().optional(),
|
||||
subfolder: z.string().optional(),
|
||||
}).optional(),
|
||||
packages: z.array(OfficialPackageSchema).optional().default([]),
|
||||
remotes: z.array(OfficialRemoteSchema).optional().default([]),
|
||||
}),
|
||||
_meta: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const OfficialRegistryResponseSchema = z.object({
|
||||
servers: z.array(OfficialServerSchema),
|
||||
metadata: z.object({
|
||||
nextCursor: z.string().nullable().optional(),
|
||||
count: z.number().optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
// Glama.ai
|
||||
const GlamaServerSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
namespace: z.string().optional().default(''),
|
||||
slug: z.string().optional().default(''),
|
||||
description: z.string().optional().default(''),
|
||||
url: z.string().optional(),
|
||||
attributes: z.array(z.string()).optional().default([]),
|
||||
repository: z.object({
|
||||
url: z.string(),
|
||||
}).optional(),
|
||||
environmentVariablesJsonSchema: z.object({
|
||||
type: z.string().optional(),
|
||||
properties: z.record(z.object({
|
||||
type: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
default: z.string().optional(),
|
||||
})).optional().default({}),
|
||||
required: z.array(z.string()).optional().default([]),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export const GlamaRegistryResponseSchema = z.object({
|
||||
servers: z.array(GlamaServerSchema),
|
||||
pageInfo: z.object({
|
||||
startCursor: z.string().nullable().optional(),
|
||||
endCursor: z.string().nullable().optional(),
|
||||
hasNextPage: z.boolean(),
|
||||
hasPreviousPage: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Smithery.ai
|
||||
const SmitheryServerSchema = z.object({
|
||||
qualifiedName: z.string(),
|
||||
displayName: z.string().optional().default(''),
|
||||
description: z.string().optional().default(''),
|
||||
iconUrl: z.string().optional(),
|
||||
verified: z.boolean().optional().default(false),
|
||||
useCount: z.number().optional().default(0),
|
||||
remote: z.boolean().optional().default(false),
|
||||
isDeployed: z.boolean().optional().default(false),
|
||||
createdAt: z.string().optional(),
|
||||
homepage: z.string().optional(),
|
||||
score: z.number().optional().default(0),
|
||||
});
|
||||
|
||||
export const SmitheryRegistryResponseSchema = z.object({
|
||||
servers: z.array(SmitheryServerSchema),
|
||||
pagination: z.object({
|
||||
currentPage: z.number(),
|
||||
pageSize: z.number(),
|
||||
totalPages: z.number(),
|
||||
totalCount: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ── Inferred types from Zod schemas ──
|
||||
|
||||
export type OfficialRegistryResponse = z.infer<typeof OfficialRegistryResponseSchema>;
|
||||
export type OfficialServerEntry = z.infer<typeof OfficialServerSchema>;
|
||||
export type GlamaRegistryResponse = z.infer<typeof GlamaRegistryResponseSchema>;
|
||||
export type GlamaServerEntry = z.infer<typeof GlamaServerSchema>;
|
||||
export type SmitheryRegistryResponse = z.infer<typeof SmitheryRegistryResponseSchema>;
|
||||
export type SmitheryServerEntry = z.infer<typeof SmitheryServerSchema>;
|
||||
|
||||
// ── Security utilities ──
|
||||
|
||||
const ANSI_ESCAPE_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F]|\x1b\[[0-9;]*[a-zA-Z]|\033\[[0-9;]*[a-zA-Z]/g;
|
||||
|
||||
export function sanitizeString(text: string): string {
|
||||
return text.replace(ANSI_ESCAPE_RE, '');
|
||||
}
|
||||
190
src/cli/tests/registry/types.test.ts
Normal file
190
src/cli/tests/registry/types.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
OfficialRegistryResponseSchema,
|
||||
GlamaRegistryResponseSchema,
|
||||
SmitheryRegistryResponseSchema,
|
||||
sanitizeString,
|
||||
} from '../../src/registry/types.js';
|
||||
|
||||
describe('sanitizeString', () => {
|
||||
it('removes ANSI escape codes (\\x1b[)', () => {
|
||||
expect(sanitizeString('\x1b[31mRED\x1b[0m text')).toBe('RED text');
|
||||
});
|
||||
|
||||
it('removes \\033[ style escape codes', () => {
|
||||
expect(sanitizeString('\x1b[1mBOLD\x1b[0m')).toBe('BOLD');
|
||||
});
|
||||
|
||||
it('removes cursor movement codes', () => {
|
||||
expect(sanitizeString('\x1b[2J\x1b[Hscreen cleared')).toBe('screen cleared');
|
||||
});
|
||||
|
||||
it('removes control characters', () => {
|
||||
expect(sanitizeString('hello\x07world')).toBe('helloworld');
|
||||
});
|
||||
|
||||
it('preserves normal text', () => {
|
||||
expect(sanitizeString('A normal MCP server description.')).toBe('A normal MCP server description.');
|
||||
});
|
||||
|
||||
it('preserves unicode characters', () => {
|
||||
expect(sanitizeString('Serveur MCP pour Slack 🚀')).toBe('Serveur MCP pour Slack 🚀');
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(sanitizeString('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OfficialRegistryResponseSchema', () => {
|
||||
it('validates a correct response', () => {
|
||||
const valid = {
|
||||
servers: [{
|
||||
server: {
|
||||
name: 'io.github.test/slack-mcp',
|
||||
description: 'Slack integration',
|
||||
packages: [{
|
||||
registryType: 'npm',
|
||||
identifier: '@test/slack-mcp',
|
||||
transport: { type: 'stdio' },
|
||||
environmentVariables: [
|
||||
{ name: 'SLACK_TOKEN', description: 'Bot token', isSecret: true },
|
||||
],
|
||||
}],
|
||||
},
|
||||
}],
|
||||
metadata: { nextCursor: 'abc:1.0.0', count: 1 },
|
||||
};
|
||||
const result = OfficialRegistryResponseSchema.safeParse(valid);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('validates response with remotes', () => {
|
||||
const valid = {
|
||||
servers: [{
|
||||
server: {
|
||||
name: 'io.github.test/remote-mcp',
|
||||
remotes: [{
|
||||
type: 'sse',
|
||||
url: 'https://example.com/sse',
|
||||
headers: [{ name: 'Authorization', isSecret: true }],
|
||||
}],
|
||||
},
|
||||
}],
|
||||
};
|
||||
const result = OfficialRegistryResponseSchema.safeParse(valid);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects response without servers array', () => {
|
||||
const result = OfficialRegistryResponseSchema.safeParse({ metadata: {} });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects server without name', () => {
|
||||
const result = OfficialRegistryResponseSchema.safeParse({
|
||||
servers: [{ server: { description: 'no name' } }],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults missing optional fields', () => {
|
||||
const minimal = {
|
||||
servers: [{ server: { name: 'test/minimal' } }],
|
||||
};
|
||||
const result = OfficialRegistryResponseSchema.parse(minimal);
|
||||
expect(result.servers[0]?.server.packages).toEqual([]);
|
||||
expect(result.servers[0]?.server.remotes).toEqual([]);
|
||||
expect(result.servers[0]?.server.description).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GlamaRegistryResponseSchema', () => {
|
||||
it('validates a correct response', () => {
|
||||
const valid = {
|
||||
servers: [{
|
||||
id: 'abc123',
|
||||
name: 'Slack MCP Server',
|
||||
description: 'Slack integration',
|
||||
attributes: ['hosting:local-only'],
|
||||
repository: { url: 'https://github.com/test/slack' },
|
||||
environmentVariablesJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
SLACK_TOKEN: { type: 'string', description: 'Bot token' },
|
||||
},
|
||||
required: ['SLACK_TOKEN'],
|
||||
},
|
||||
}],
|
||||
pageInfo: {
|
||||
endCursor: 'xyz',
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
};
|
||||
const result = GlamaRegistryResponseSchema.safeParse(valid);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects response without pageInfo', () => {
|
||||
const result = GlamaRegistryResponseSchema.safeParse({
|
||||
servers: [{ id: 'a', name: 'test' }],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults missing env schema properties', () => {
|
||||
const minimal = {
|
||||
servers: [{
|
||||
id: 'a',
|
||||
name: 'test',
|
||||
environmentVariablesJsonSchema: {},
|
||||
}],
|
||||
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
||||
};
|
||||
const result = GlamaRegistryResponseSchema.parse(minimal);
|
||||
const envSchema = result.servers[0]?.environmentVariablesJsonSchema;
|
||||
expect(envSchema?.properties).toEqual({});
|
||||
expect(envSchema?.required).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SmitheryRegistryResponseSchema', () => {
|
||||
it('validates a correct response', () => {
|
||||
const valid = {
|
||||
servers: [{
|
||||
qualifiedName: 'slack',
|
||||
displayName: 'Slack',
|
||||
description: 'Slack integration',
|
||||
verified: true,
|
||||
useCount: 14062,
|
||||
remote: true,
|
||||
}],
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
totalPages: 5,
|
||||
totalCount: 50,
|
||||
},
|
||||
};
|
||||
const result = SmitheryRegistryResponseSchema.safeParse(valid);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects response without pagination', () => {
|
||||
const result = SmitheryRegistryResponseSchema.safeParse({
|
||||
servers: [{ qualifiedName: 'test' }],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults useCount and verified', () => {
|
||||
const minimal = {
|
||||
servers: [{ qualifiedName: 'test' }],
|
||||
pagination: { currentPage: 1, pageSize: 10, totalPages: 1, totalCount: 1 },
|
||||
};
|
||||
const result = SmitheryRegistryResponseSchema.parse(minimal);
|
||||
expect(result.servers[0]?.useCount).toBe(0);
|
||||
expect(result.servers[0]?.verified).toBe(false);
|
||||
});
|
||||
});
|
||||
12
src/cli/tsconfig.json
Normal file
12
src/cli/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
{ "path": "../shared" },
|
||||
{ "path": "../db" }
|
||||
]
|
||||
}
|
||||
8
src/cli/vitest.config.ts
Normal file
8
src/cli/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
name: 'cli',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
31
src/db/package.json
Normal file
31
src/db/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@mcpctl/db",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"clean": "rimraf dist",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"db:generate": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:seed": "tsx src/seed/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.0.0",
|
||||
"@mcpctl/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^6.0.0"
|
||||
}
|
||||
}
|
||||
2
src/db/src/index.ts
Normal file
2
src/db/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Database package - Prisma client and utilities
|
||||
// Will be implemented in Task 2
|
||||
11
src/db/tsconfig.json
Normal file
11
src/db/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
{ "path": "../shared" }
|
||||
]
|
||||
}
|
||||
8
src/db/vitest.config.ts
Normal file
8
src/db/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
name: 'db',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
20
src/local-proxy/package.json
Normal file
20
src/local-proxy/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@mcpctl/local-proxy",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"@mcpctl/shared": "workspace:*"
|
||||
}
|
||||
}
|
||||
2
src/local-proxy/src/index.ts
Normal file
2
src/local-proxy/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Local LLM proxy entry point
|
||||
// Will be implemented in Task 11
|
||||
11
src/local-proxy/tsconfig.json
Normal file
11
src/local-proxy/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
{ "path": "../shared" }
|
||||
]
|
||||
}
|
||||
8
src/local-proxy/vitest.config.ts
Normal file
8
src/local-proxy/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
name: 'local-proxy',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
25
src/mcpd/package.json
Normal file
25
src/mcpd/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@mcpctl/mcpd",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"fastify": "^5.0.0",
|
||||
"@fastify/cors": "^10.0.0",
|
||||
"@fastify/helmet": "^12.0.0",
|
||||
"@fastify/rate-limit": "^10.0.0",
|
||||
"zod": "^3.24.0",
|
||||
"@mcpctl/shared": "workspace:*",
|
||||
"@mcpctl/db": "workspace:*"
|
||||
}
|
||||
}
|
||||
2
src/mcpd/src/index.ts
Normal file
2
src/mcpd/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// mcpd daemon server entry point
|
||||
// Will be implemented in Task 3
|
||||
12
src/mcpd/tsconfig.json
Normal file
12
src/mcpd/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
{ "path": "../shared" },
|
||||
{ "path": "../db" }
|
||||
]
|
||||
}
|
||||
8
src/mcpd/vitest.config.ts
Normal file
8
src/mcpd/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
name: 'mcpd',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
23
src/shared/package.json
Normal file
23
src/shared/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@mcpctl/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"clean": "rimraf dist",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.0"
|
||||
}
|
||||
}
|
||||
5
src/shared/src/constants/index.ts
Normal file
5
src/shared/src/constants/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Shared constants
|
||||
export const APP_NAME = 'mcpctl';
|
||||
export const APP_VERSION = '0.1.0';
|
||||
export const DEFAULT_MCPD_URL = 'http://localhost:3000';
|
||||
export const DEFAULT_DB_PORT = 5432;
|
||||
4
src/shared/src/index.ts
Normal file
4
src/shared/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './types/index.js';
|
||||
export * from './validation/index.js';
|
||||
export * from './constants/index.js';
|
||||
export * from './utils/index.js';
|
||||
46
src/shared/src/types/index.ts
Normal file
46
src/shared/src/types/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// Core domain types for mcpctl
|
||||
// These will be expanded as tasks are implemented
|
||||
|
||||
export interface McpServerConfig {
|
||||
name: string;
|
||||
type: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
envTemplate: EnvTemplateEntry[];
|
||||
setupGuide?: string;
|
||||
}
|
||||
|
||||
export interface EnvTemplateEntry {
|
||||
name: string;
|
||||
description: string;
|
||||
isSecret: boolean;
|
||||
setupUrl?: string;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export interface McpProfile {
|
||||
name: string;
|
||||
serverId: string;
|
||||
config: Record<string, unknown>;
|
||||
filterRules?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface McpProject {
|
||||
name: string;
|
||||
description?: string;
|
||||
profileIds: string[];
|
||||
}
|
||||
|
||||
// Service interfaces for dependency injection
|
||||
export interface BackupService {
|
||||
exportConfig(): Promise<string>;
|
||||
importConfig(data: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ConfigExporter {
|
||||
serialize(): Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface ConfigImporter {
|
||||
deserialize(data: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
2
src/shared/src/utils/index.ts
Normal file
2
src/shared/src/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Shared utility functions
|
||||
// Will be expanded as tasks are implemented
|
||||
4
src/shared/src/validation/index.ts
Normal file
4
src/shared/src/validation/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Shared Zod validation schemas
|
||||
// Will be expanded as tasks are implemented
|
||||
|
||||
export { z } from 'zod';
|
||||
16
src/shared/tests/index.test.ts
Normal file
16
src/shared/tests/index.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { APP_NAME, APP_VERSION, DEFAULT_MCPD_URL } from '../src/constants/index.js';
|
||||
|
||||
describe('shared package', () => {
|
||||
it('exports APP_NAME constant', () => {
|
||||
expect(APP_NAME).toBe('mcpctl');
|
||||
});
|
||||
|
||||
it('exports APP_VERSION constant', () => {
|
||||
expect(APP_VERSION).toBe('0.1.0');
|
||||
});
|
||||
|
||||
it('exports DEFAULT_MCPD_URL constant', () => {
|
||||
expect(DEFAULT_MCPD_URL).toBe('http://localhost:3000');
|
||||
});
|
||||
});
|
||||
8
src/shared/tsconfig.json
Normal file
8
src/shared/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
8
src/shared/vitest.config.ts
Normal file
8
src/shared/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
name: 'shared',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user