feat: add OpenProject MCP server source

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 16:56:56 +08:00
parent e9b2135616
commit f23296ec82
8 changed files with 3783 additions and 0 deletions
+1716
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@kollect/openproject-mcp",
"version": "0.1.0",
"description": "MCP server for OpenProject integration",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}
+1106
View File
File diff suppressed because it is too large Load Diff
+242
View File
@@ -0,0 +1,242 @@
/**
* OpenProject API v3 HTTP client with Basic Auth (API key).
*/
export interface OpenProjectConfig {
baseUrl: string;
apiKey: string;
}
export interface PaginatedResponse<T> {
_type: string;
total: number;
count: number;
pageSize: number;
offset: number;
_embedded: { elements: T[] };
}
export class OpenProjectClient {
private baseUrl: string;
private headers: Record<string, string>;
constructor(config: OpenProjectConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "") + "/api/v3";
const credentials = Buffer.from(`apikey:${config.apiKey}`).toString(
"base64",
);
this.headers = {
Authorization: `Basic ${credentials}`,
"Content-Type": "application/json",
};
}
async request<T>(
method: string,
path: string,
body?: unknown,
params?: Record<string, string>,
): Promise<T> {
const url = new URL(`${this.baseUrl}${path}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
}
const response = await fetch(url.toString(), {
method,
headers: this.headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`OpenProject API ${method} ${path} failed (${response.status}): ${errorBody}`,
);
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
// --- Projects ---
async listProjects(
offset = 1,
pageSize = 20,
): Promise<PaginatedResponse<any>> {
return this.request("GET", "/projects", undefined, {
offset: String(offset),
pageSize: String(pageSize),
});
}
async getProject(id: number | string): Promise<any> {
return this.request("GET", `/projects/${id}`);
}
// --- Work Packages ---
async listWorkPackages(
projectId?: number | string,
filters?: any[],
offset = 1,
pageSize = 20,
): Promise<PaginatedResponse<any>> {
const path = projectId
? `/projects/${projectId}/work_packages`
: "/work_packages";
const params: Record<string, string> = {
offset: String(offset),
pageSize: String(pageSize),
};
if (filters) {
params.filters = JSON.stringify(filters);
}
return this.request("GET", path, undefined, params);
}
async getWorkPackage(id: number): Promise<any> {
return this.request("GET", `/work_packages/${id}`);
}
async createWorkPackage(projectId: number | string, data: any): Promise<any> {
return this.request("POST", `/projects/${projectId}/work_packages`, data);
}
async updateWorkPackage(id: number, data: any): Promise<any> {
// Fetch current lockVersion for optimistic concurrency control
const current = await this.getWorkPackage(id);
data.lockVersion = current.lockVersion;
return this.request("PATCH", `/work_packages/${id}`, data);
}
// --- Statuses ---
async listStatuses(): Promise<PaginatedResponse<any>> {
return this.request("GET", "/statuses");
}
// --- Types ---
async listTypes(): Promise<PaginatedResponse<any>> {
return this.request("GET", "/types");
}
// --- Users ---
async getMe(): Promise<any> {
return this.request("GET", "/users/me");
}
async listUsers(
offset = 1,
pageSize = 20,
): Promise<PaginatedResponse<any>> {
return this.request("GET", "/users", undefined, {
offset: String(offset),
pageSize: String(pageSize),
});
}
// --- Time Entries ---
async listTimeEntries(
projectId?: number | string,
offset = 1,
pageSize = 20,
): Promise<PaginatedResponse<any>> {
const params: Record<string, string> = {
offset: String(offset),
pageSize: String(pageSize),
};
if (projectId) {
params.filters = JSON.stringify([
{ project: { operator: "=", values: [String(projectId)] } },
]);
}
return this.request("GET", "/time_entries", undefined, params);
}
async createTimeEntry(data: any): Promise<any> {
return this.request("POST", "/time_entries", data);
}
// --- Priorities ---
async listPriorities(): Promise<PaginatedResponse<any>> {
return this.request("GET", "/priorities");
}
// --- Versions ---
async listVersions(
projectId: number | string,
): Promise<PaginatedResponse<any>> {
return this.request("GET", `/projects/${projectId}/versions`);
}
// --- Relations ---
async listRelations(
workPackageId: number,
): Promise<PaginatedResponse<any>> {
return this.request(
"GET",
"/relations",
undefined,
{
filters: JSON.stringify([
{ involved: { operator: "=", values: [String(workPackageId)] } },
]),
pageSize: "100",
},
);
}
async createRelation(
fromId: number,
toId: number,
type: string,
description?: string,
): Promise<any> {
return this.request(
"POST",
`/work_packages/${fromId}/relations`,
{
_links: {
from: { href: `/api/v3/work_packages/${fromId}` },
to: { href: `/api/v3/work_packages/${toId}` },
},
type,
...(description ? { description } : {}),
},
);
}
async deleteRelation(relationId: number): Promise<void> {
return this.request("DELETE", `/relations/${relationId}`);
}
// --- Activities ---
async listActivities(workPackageId: number): Promise<PaginatedResponse<any>> {
return this.request(
"GET",
`/work_packages/${workPackageId}/activities`,
);
}
async addComment(workPackageId: number, comment: string): Promise<any> {
return this.request(
"POST",
`/work_packages/${workPackageId}/activities`,
{ comment: { raw: comment } },
);
}
}
+342
View File
@@ -0,0 +1,342 @@
/**
* Tool handler implementations — maps MCP tool calls to OpenProject API client methods.
*/
import { OpenProjectClient } from "./client.js";
/** Format a HAL+JSON work package into a readable summary. */
function formatWorkPackage(wp: any): string {
const status = wp._links?.status?.title ?? "Unknown";
const type = wp._links?.type?.title ?? "Unknown";
const assignee = wp._links?.assignee?.title ?? "Unassigned";
const priority = wp._links?.priority?.title ?? "Normal";
return [
`#${wp.id}: ${wp.subject}`,
` Type: ${type} | Status: ${status} | Priority: ${priority}`,
` Assignee: ${assignee}`,
wp.startDate ? ` Start: ${wp.startDate}` : null,
wp.dueDate ? ` Due: ${wp.dueDate}` : null,
wp.percentageDone != null ? ` Progress: ${wp.percentageDone}%` : null,
wp.description?.raw ? ` Description: ${wp.description.raw.slice(0, 200)}` : null,
]
.filter(Boolean)
.join("\n");
}
/** Format a HAL+JSON project into a readable summary. */
function formatProject(p: any): string {
const status = p.status ?? p._links?.status?.title ?? "Unknown";
return [
`${p.name} (${p.identifier})`,
` ID: ${p.id} | Status: ${typeof status === "object" ? status.code ?? status : status}`,
p.description?.raw ? ` Description: ${p.description.raw.slice(0, 200)}` : null,
]
.filter(Boolean)
.join("\n");
}
function formatPaginatedList(
response: any,
formatter: (item: any) => string,
label: string,
): string {
const elements = response._embedded?.elements ?? [];
if (elements.length === 0) {
return `No ${label} found.`;
}
const header = `${label} (${response.offset}${response.offset + response.count - 1} of ${response.total})`;
return [header, "", ...elements.map(formatter)].join("\n");
}
export function createHandlers(client: OpenProjectClient) {
return {
// --- Projects ---
async list_projects(args: { offset?: number; pageSize?: number }) {
const result = await client.listProjects(args.offset, args.pageSize);
return formatPaginatedList(result, formatProject, "Projects");
},
async get_project(args: { id: number | string }) {
const project = await client.getProject(args.id);
return formatProject(project);
},
// --- Work Packages ---
async list_work_packages(args: {
projectId?: number | string;
statusId?: string[];
typeId?: string[];
assigneeId?: string[];
offset?: number;
pageSize?: number;
}) {
const filters: any[] = [];
if (args.statusId?.length) {
filters.push({
status_id: { operator: "=", values: args.statusId },
});
}
if (args.typeId?.length) {
filters.push({ type_id: { operator: "=", values: args.typeId } });
}
if (args.assigneeId?.length) {
filters.push({
assignee: { operator: "=", values: args.assigneeId },
});
}
const result = await client.listWorkPackages(
args.projectId,
filters.length > 0 ? filters : undefined,
args.offset,
args.pageSize,
);
return formatPaginatedList(result, formatWorkPackage, "Work Packages");
},
async get_work_package(args: { id: number }) {
const wp = await client.getWorkPackage(args.id);
return formatWorkPackage(wp);
},
async create_work_package(args: {
projectId: number | string;
subject: string;
description?: string;
typeHref?: string;
statusHref?: string;
assigneeHref?: string;
priorityHref?: string;
startDate?: string;
dueDate?: string;
estimatedTime?: string;
}) {
const payload: any = {
subject: args.subject,
_links: {},
};
if (args.description) {
payload.description = { raw: args.description };
}
if (args.typeHref) {
payload._links.type = { href: args.typeHref };
}
if (args.statusHref) {
payload._links.status = { href: args.statusHref };
}
if (args.assigneeHref) {
payload._links.assignee = { href: args.assigneeHref };
}
if (args.priorityHref) {
payload._links.priority = { href: args.priorityHref };
}
if (args.startDate) payload.startDate = args.startDate;
if (args.dueDate) payload.dueDate = args.dueDate;
if (args.estimatedTime) payload.estimatedTime = args.estimatedTime;
const wp = await client.createWorkPackage(args.projectId, payload);
return `Created work package:\n${formatWorkPackage(wp)}`;
},
async update_work_package(args: {
id: number;
subject?: string;
description?: string;
statusHref?: string;
assigneeHref?: string;
priorityHref?: string;
startDate?: string;
dueDate?: string;
estimatedTime?: string;
percentageDone?: number;
}) {
const payload: any = { _links: {} };
if (args.subject) payload.subject = args.subject;
if (args.description) payload.description = { raw: args.description };
if (args.statusHref) payload._links.status = { href: args.statusHref };
if (args.assigneeHref)
payload._links.assignee = { href: args.assigneeHref };
if (args.priorityHref)
payload._links.priority = { href: args.priorityHref };
if (args.startDate) payload.startDate = args.startDate;
if (args.dueDate) payload.dueDate = args.dueDate;
if (args.estimatedTime) payload.estimatedTime = args.estimatedTime;
if (args.percentageDone != null)
payload.percentageDone = args.percentageDone;
// Remove empty _links
if (Object.keys(payload._links).length === 0) delete payload._links;
const wp = await client.updateWorkPackage(args.id, payload);
return `Updated work package:\n${formatWorkPackage(wp)}`;
},
async add_work_package_comment(args: {
workPackageId: number;
comment: string;
}) {
await client.addComment(args.workPackageId, args.comment);
return `Comment added to work package #${args.workPackageId}.`;
},
// --- Relations ---
async list_relations(args: { workPackageId: number }) {
const result = await client.listRelations(args.workPackageId);
const elements = result._embedded?.elements ?? [];
if (elements.length === 0) {
return `No relations found for work package #${args.workPackageId}.`;
}
return elements
.map((r: any) => {
const fromId = r._links?.from?.href?.split("/").pop() ?? "?";
const toId = r._links?.to?.href?.split("/").pop() ?? "?";
const fromTitle = r._links?.from?.title ?? `#${fromId}`;
const toTitle = r._links?.to?.title ?? `#${toId}`;
return `${r.id}: ${r.type} — #${fromId} (${fromTitle}) → #${toId} (${toTitle})${r.description ? ` "${r.description}"` : ""}`;
})
.join("\n");
},
async create_relation(args: {
fromId: number;
toId: number;
type: string;
description?: string;
}) {
const rel = await client.createRelation(
args.fromId,
args.toId,
args.type,
args.description,
);
const fromId = rel._links?.from?.href?.split("/").pop() ?? "?";
const toId = rel._links?.to?.href?.split("/").pop() ?? "?";
return `Created relation #${rel.id}: ${rel.type} — #${fromId} → #${toId}`;
},
async delete_relation(args: { relationId: number }) {
await client.deleteRelation(args.relationId);
return `Deleted relation #${args.relationId}.`;
},
// --- Lookup ---
async list_statuses() {
const result = await client.listStatuses();
const elements = result._embedded?.elements ?? [];
return elements
.map((s: any) => `${s.id}: ${s.name} (href: /api/v3/statuses/${s.id})`)
.join("\n") || "No statuses found.";
},
async list_types() {
const result = await client.listTypes();
const elements = result._embedded?.elements ?? [];
return elements
.map((t: any) => `${t.id}: ${t.name} (href: /api/v3/types/${t.id})`)
.join("\n") || "No types found.";
},
async list_priorities() {
const result = await client.listPriorities();
const elements = result._embedded?.elements ?? [];
return elements
.map(
(p: any) =>
`${p.id}: ${p.name} (href: /api/v3/priorities/${p.id})`,
)
.join("\n") || "No priorities found.";
},
// --- Users ---
async get_me() {
const user = await client.getMe();
return [
`Authenticated as: ${user.name} (${user.login})`,
` ID: ${user.id}`,
` Email: ${user.email ?? "N/A"}`,
` Admin: ${user.admin ?? false}`,
` Status: ${user.status}`,
].join("\n");
},
async list_users(args: { offset?: number; pageSize?: number }) {
const result = await client.listUsers(args.offset, args.pageSize);
const elements = result._embedded?.elements ?? [];
return formatPaginatedList(
result,
(u: any) =>
`${u.id}: ${u.name} (${u.login}) — ${u.status} (href: /api/v3/users/${u.id})`,
"Users",
);
},
// --- Time Entries ---
async list_time_entries(args: {
projectId?: number | string;
offset?: number;
pageSize?: number;
}) {
const result = await client.listTimeEntries(
args.projectId,
args.offset,
args.pageSize,
);
return formatPaginatedList(
result,
(te: any) => {
const wp = te._links?.workPackage?.title ?? "N/A";
const user = te._links?.user?.title ?? "N/A";
return `${te.id}: ${te.hours} on "${wp}" by ${user} (${te.spentOn})`;
},
"Time Entries",
);
},
async log_time(args: {
workPackageId: number;
hours: string;
activityHref: string;
comment?: string;
spentOn?: string;
}) {
const payload: any = {
hours: args.hours,
comment: args.comment ? { raw: args.comment } : undefined,
spentOn: args.spentOn ?? new Date().toISOString().split("T")[0],
_links: {
workPackage: { href: `/api/v3/work_packages/${args.workPackageId}` },
activity: { href: args.activityHref },
},
};
const te = await client.createTimeEntry(payload);
return `Time entry #${te.id} logged: ${te.hours} on work package #${args.workPackageId}.`;
},
// --- Versions ---
async list_versions(args: { projectId: number | string }) {
const result = await client.listVersions(args.projectId);
const elements = result._embedded?.elements ?? [];
return elements
.map(
(v: any) =>
`${v.id}: ${v.name}${v.status} (start: ${v.startDate ?? "N/A"}, due: ${v.endDate ?? "N/A"})`,
)
.join("\n") || "No versions found.";
},
// --- Activities ---
async list_work_package_activities(args: { workPackageId: number }) {
const result = await client.listActivities(args.workPackageId);
const elements = result._embedded?.elements ?? [];
return elements
.map((a: any) => {
const user = a._links?.user?.title ?? "System";
const date = a.createdAt?.split("T")[0] ?? "Unknown";
const comment = a.comment?.raw
? `\n "${a.comment.raw.slice(0, 150)}"`
: "";
return `${a.id}: ${date} by ${user}${comment}`;
})
.join("\n") || "No activities found.";
},
};
}
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env node
/**
* OpenProject MCP Server
*
* Connects Claude to OpenProject via the v3 REST API.
*
* Environment variables:
* OPENPROJECT_URL — Base URL of your OpenProject instance (e.g. https://op.example.com)
* OPENPROJECT_API_KEY — API key (generated under My Account > Access Tokens)
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { OpenProjectClient } from "./client.js";
import { toolDefs } from "./tools.js";
import { createHandlers } from "./handlers.js";
function getEnvOrThrow(name: string): string {
const value = process.env[name];
if (!value) {
console.error(`Missing required environment variable: ${name}`);
process.exit(1);
}
return value;
}
const client = new OpenProjectClient({
baseUrl: getEnvOrThrow("OPENPROJECT_URL"),
apiKey: getEnvOrThrow("OPENPROJECT_API_KEY"),
});
const handlers = createHandlers(client);
const server = new McpServer({
name: "openproject",
version: "0.1.0",
});
// Register each tool from toolDefs with its corresponding handler.
for (const [name, def] of Object.entries(toolDefs)) {
const handler = handlers[name as keyof typeof handlers];
if (!handler) {
console.error(`No handler for tool: ${name}`);
continue;
}
server.tool(
name,
def.description,
def.inputSchema,
async (args: any) => {
try {
const result = await (handler as Function)(args);
return { content: [{ type: "text" as const, text: String(result) }] };
} catch (err: any) {
return {
content: [{ type: "text" as const, text: `Error: ${err.message}` }],
isError: true,
};
}
},
);
}
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("OpenProject MCP server running on stdio");
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});
+268
View File
@@ -0,0 +1,268 @@
/**
* MCP tool definitions for OpenProject operations.
*/
import { z } from "zod";
export const toolDefs = {
// --- Projects ---
list_projects: {
description:
"List all accessible OpenProject projects. Returns project name, identifier, status, and description.",
inputSchema: {
offset: z
.number()
.optional()
.default(1)
.describe("Page number (1-based)"),
pageSize: z
.number()
.optional()
.default(20)
.describe("Items per page (max 200)"),
},
},
get_project: {
description:
"Get detailed information about a specific OpenProject project by ID or identifier.",
inputSchema: {
id: z
.union([z.number(), z.string()])
.describe("Project numeric ID or string identifier"),
},
},
// --- Work Packages ---
list_work_packages: {
description:
"List work packages (tasks, bugs, features, etc.) with optional filtering by project, status, type, or assignee.",
inputSchema: {
projectId: z
.union([z.number(), z.string()])
.optional()
.describe("Filter by project ID or identifier"),
statusId: z
.array(z.string())
.optional()
.describe("Filter by status IDs"),
typeId: z
.array(z.string())
.optional()
.describe("Filter by type IDs"),
assigneeId: z
.array(z.string())
.optional()
.describe("Filter by assignee user IDs"),
offset: z.number().optional().default(1),
pageSize: z.number().optional().default(20),
},
},
get_work_package: {
description:
"Get detailed information about a specific work package including status, assignee, description, and custom fields.",
inputSchema: {
id: z.number().describe("Work package ID"),
},
},
create_work_package: {
description:
"Create a new work package (task, bug, feature, etc.) in a project.",
inputSchema: {
projectId: z
.union([z.number(), z.string()])
.describe("Project ID or identifier"),
subject: z.string().describe("Work package title/subject"),
description: z
.string()
.optional()
.describe("Work package description (markdown supported)"),
typeHref: z
.string()
.optional()
.describe("Type API href, e.g. /api/v3/types/1"),
statusHref: z
.string()
.optional()
.describe("Status API href, e.g. /api/v3/statuses/1"),
assigneeHref: z
.string()
.optional()
.describe("Assignee API href, e.g. /api/v3/users/5"),
priorityHref: z
.string()
.optional()
.describe("Priority API href, e.g. /api/v3/priorities/2"),
startDate: z
.string()
.optional()
.describe("Start date (YYYY-MM-DD)"),
dueDate: z.string().optional().describe("Due date (YYYY-MM-DD)"),
estimatedTime: z
.string()
.optional()
.describe("Estimated time in ISO 8601 duration, e.g. PT2H"),
},
},
update_work_package: {
description:
"Update an existing work package. Only provided fields are changed.",
inputSchema: {
id: z.number().describe("Work package ID"),
subject: z.string().optional().describe("New title/subject"),
description: z.string().optional().describe("New description"),
statusHref: z.string().optional().describe("New status API href"),
assigneeHref: z.string().optional().describe("New assignee API href"),
priorityHref: z.string().optional().describe("New priority API href"),
startDate: z.string().optional().describe("New start date (YYYY-MM-DD)"),
dueDate: z.string().optional().describe("New due date (YYYY-MM-DD)"),
estimatedTime: z
.string()
.optional()
.describe("New estimated time (ISO 8601 duration)"),
percentageDone: z
.number()
.optional()
.describe("Completion percentage (0-100)"),
},
},
add_work_package_comment: {
description: "Add a comment to a work package.",
inputSchema: {
workPackageId: z.number().describe("Work package ID"),
comment: z.string().describe("Comment text (markdown supported)"),
},
},
// --- Relations ---
list_relations: {
description:
"List all relations for a work package (requires, follows, blocks, etc.).",
inputSchema: {
workPackageId: z.number().describe("Work package ID"),
},
},
create_relation: {
description:
"Create a relation between two work packages. Types: requires, follows, blocks, relates, includes, duplicates.",
inputSchema: {
fromId: z.number().describe("Source work package ID"),
toId: z.number().describe("Target work package ID"),
type: z
.enum([
"relates",
"duplicates",
"duplicated",
"blocks",
"blocked",
"precedes",
"follows",
"includes",
"partof",
"requires",
"required",
])
.describe("Relation type"),
description: z
.string()
.optional()
.describe("Optional description of the relation"),
},
},
delete_relation: {
description: "Delete a relation by its ID.",
inputSchema: {
relationId: z.number().describe("Relation ID to delete"),
},
},
// --- Lookup / Reference ---
list_statuses: {
description:
"List all available work package statuses (e.g., New, In Progress, Closed). Use to discover status hrefs for filtering or updating.",
inputSchema: {},
},
list_types: {
description:
"List all available work package types (e.g., Task, Bug, Feature, Epic). Use to discover type hrefs.",
inputSchema: {},
},
list_priorities: {
description:
"List all available priorities (e.g., Low, Normal, High, Urgent). Use to discover priority hrefs.",
inputSchema: {},
},
// --- Users ---
get_me: {
description:
"Get the currently authenticated user's profile. Useful for verifying the connection.",
inputSchema: {},
},
list_users: {
description: "List all users in the OpenProject instance.",
inputSchema: {
offset: z.number().optional().default(1),
pageSize: z.number().optional().default(20),
},
},
// --- Time Entries ---
list_time_entries: {
description:
"List time entries, optionally filtered by project.",
inputSchema: {
projectId: z
.union([z.number(), z.string()])
.optional()
.describe("Filter by project ID"),
offset: z.number().optional().default(1),
pageSize: z.number().optional().default(20),
},
},
log_time: {
description: "Log time spent on a work package.",
inputSchema: {
workPackageId: z.number().describe("Work package ID"),
hours: z.string().describe("Time in ISO 8601 duration, e.g. PT1H30M"),
activityHref: z
.string()
.describe("Activity type API href, e.g. /api/v3/time_entries/activities/1"),
comment: z.string().optional().describe("Time entry comment"),
spentOn: z
.string()
.optional()
.describe("Date spent (YYYY-MM-DD, defaults to today)"),
},
},
// --- Versions ---
list_versions: {
description:
"List versions/milestones for a project.",
inputSchema: {
projectId: z
.union([z.number(), z.string()])
.describe("Project ID or identifier"),
},
},
// --- Activities ---
list_work_package_activities: {
description:
"List the activity/journal history of a work package (comments, status changes, etc.).",
inputSchema: {
workPackageId: z.number().describe("Work package ID"),
},
},
} as const;
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}