Compare commits
25 commits
no-more-re
...
main
Author | SHA1 | Date | |
---|---|---|---|
d1cb48f770 | |||
a63b93aa5c | |||
d8ce274506 | |||
a7a76d3f17 | |||
27907a4624 | |||
0213c20d3d | |||
4f6de14fbc | |||
675dc6a760 | |||
8f83b18966 | |||
|
7eecc9ca3f | ||
|
5199279ea7 | ||
|
b1dfa9fe5b | ||
|
6f2399bbfb | ||
|
e5cdf08bc8 | ||
|
bd6df4222f | ||
|
b209d98074 | ||
|
bd75a1cdf0 | ||
|
767549412f | ||
|
46e3333416 | ||
|
63e02eef1c | ||
|
45c1739b4c | ||
|
5c71b26869 | ||
|
b9105958c1 | ||
|
114ee3c4b6 | ||
|
c9e082d9e2 |
9 changed files with 119 additions and 87 deletions
21
.github/workflows/publish.yml
vendored
21
.github/workflows/publish.yml
vendored
|
@ -1,11 +1,12 @@
|
|||
name: Publish Any Commit
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
tags:
|
||||
- "!**"
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -14,17 +15,19 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Add git.kvant.cloud scope
|
||||
run: npm config set @kvant:registry=https://git.kvant.cloud/api/packages/${{ github.repository_owner }}/npm/
|
||||
|
||||
- name: Login to git.kvant.cloud npm
|
||||
run: npm config set -- '//git.kvant.cloud/api/packages/${{ github.repository_owner }}/npm/:_authToken' "${{ secrets.PHOENIX_PACKAGE_WRITER_TOKEN }}"
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Setup pnpm & install
|
||||
uses: https://github.com/wyvox/action-setup-pnpm@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
node-version: 22
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- run: pnpm dlx pkg-pr-new publish --compact --bin
|
||||
- run: pnpm dlx publish --compact --bin
|
||||
|
|
18
README.md
18
README.md
|
@ -46,16 +46,16 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
|||
"https://remote.mcp.server/sse",
|
||||
"--header",
|
||||
"Authorization: Bearer ${AUTH_TOKEN}"
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"AUTH_TOKEN": "..."
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"AUTH_TOKEN": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Cursor has a bug where spaces inside `args` aren't escaped when it invokes `npx`, which ends up mangling these values. You can work around it using:
|
||||
**Note:** Cursor and Claude Desktop (Windows) have a bug where spaces inside `args` aren't escaped when it invokes `npx`, which ends up mangling these values. You can work around it using:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
|
@ -65,11 +65,11 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
|||
"https://remote.mcp.server/sse",
|
||||
"--header",
|
||||
"Authorization:${AUTH_HEADER}" // note no spaces around ':'
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"AUTH_HEADER": "Bearer <auth-token>" // spaces OK in env vars
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"AUTH_HEADER": "Bearer <auth-token>" // spaces OK in env vars
|
||||
}
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
|
14
package.json
14
package.json
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"name": "mcp-remote",
|
||||
"version": "0.1.2",
|
||||
"packageManager": "pnpm@8.15.1",
|
||||
"name": "@kvant/mcp-remote",
|
||||
"version": "0.1.5",
|
||||
"description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
|
||||
"keywords": [
|
||||
"mcp",
|
||||
|
@ -32,13 +31,12 @@
|
|||
"express": "^4.21.2",
|
||||
"open": "^10.1.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"devDependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/react": "^19.0.12",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^19.0.0",
|
||||
"tsup": "^8.4.0",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
|
@ -54,8 +52,6 @@
|
|||
"dts": true,
|
||||
"clean": true,
|
||||
"outDir": "dist",
|
||||
"external": [
|
||||
"react"
|
||||
]
|
||||
"external": []
|
||||
}
|
||||
}
|
||||
|
|
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
|
@ -16,23 +16,17 @@ importers:
|
|||
version: 10.1.0
|
||||
devDependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.10.2
|
||||
version: 1.10.2
|
||||
specifier: ^1.11.2
|
||||
version: 1.11.2
|
||||
'@types/express':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
'@types/node':
|
||||
specifier: ^22.13.10
|
||||
version: 22.13.10
|
||||
'@types/react':
|
||||
specifier: ^19.0.12
|
||||
version: 19.0.12
|
||||
prettier:
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0
|
||||
tsup:
|
||||
specifier: ^8.4.0
|
||||
version: 8.4.0(tsx@4.19.3)(typescript@5.8.2)
|
||||
|
@ -217,8 +211,8 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.10.2':
|
||||
resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==}
|
||||
'@modelcontextprotocol/sdk@1.11.2':
|
||||
resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
|
@ -350,9 +344,6 @@ packages:
|
|||
'@types/range-parser@1.2.7':
|
||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||
|
||||
'@types/react@19.0.12':
|
||||
resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
|
||||
|
||||
|
@ -479,9 +470,6 @@ packages:
|
|||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
|
@ -899,10 +887,6 @@ packages:
|
|||
resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
react@19.0.0:
|
||||
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
@ -1222,7 +1206,7 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@modelcontextprotocol/sdk@1.10.2':
|
||||
'@modelcontextprotocol/sdk@1.11.2':
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
cors: 2.8.5
|
||||
|
@ -1334,10 +1318,6 @@ snapshots:
|
|||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
|
||||
'@types/react@19.0.12':
|
||||
dependencies:
|
||||
csstype: 3.1.3
|
||||
|
||||
'@types/send@0.17.4':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
|
@ -1474,8 +1454,6 @@ snapshots:
|
|||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
|
@ -1896,8 +1874,6 @@ snapshots:
|
|||
iconv-lite: 0.6.3
|
||||
unpipe: 1.0.0
|
||||
|
||||
react@19.0.0: {}
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
|
|
@ -151,7 +151,7 @@ async function runClient(
|
|||
}
|
||||
|
||||
// Parse command-line arguments and run the client
|
||||
parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
|
||||
parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
|
||||
.then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
|
||||
return runClient(serverUrl, callbackPort, headers, transportStrategy)
|
||||
})
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import open from 'open'
|
||||
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||
import {
|
||||
OAuthClientInformation,
|
||||
OAuthClientInformationFull,
|
||||
OAuthClientInformationSchema,
|
||||
OAuthClientInformationFullSchema,
|
||||
OAuthTokens,
|
||||
OAuthTokensSchema,
|
||||
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||
|
@ -37,7 +36,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
|||
}
|
||||
|
||||
get redirectUrl(): string {
|
||||
return `http://127.0.0.1:${this.options.callbackPort}${this.callbackPath}`
|
||||
return `http://localhost:${this.options.callbackPort}${this.callbackPath}`
|
||||
}
|
||||
|
||||
get clientMetadata() {
|
||||
|
@ -57,9 +56,9 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
|||
* Gets the client information if it exists
|
||||
* @returns The client information or undefined
|
||||
*/
|
||||
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
||||
async clientInformation(): Promise<OAuthClientInformationFull | undefined> {
|
||||
// log('Reading client info')
|
||||
return readJsonFile<OAuthClientInformation>(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema)
|
||||
return readJsonFile<OAuthClientInformationFull>(this.serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
104
src/lib/utils.ts
104
src/lib/utils.ts
|
@ -3,6 +3,13 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
||||
import { OAuthClientInformationFull, OAuthClientInformationFullSchema } from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||
import { OAuthCallbackServerOptions } from './types'
|
||||
import { getConfigFilePath, readJsonFile } from './mcp-auth-config'
|
||||
import express from 'express'
|
||||
import net from 'net'
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs/promises'
|
||||
|
||||
// Connection constants
|
||||
export const REASON_AUTH_NEEDED = 'authentication-needed'
|
||||
|
@ -10,10 +17,6 @@ export const REASON_TRANSPORT_FALLBACK = 'falling-back-to-alternate-transport'
|
|||
|
||||
// Transport strategy types
|
||||
export type TransportStrategy = 'sse-only' | 'http-only' | 'sse-first' | 'http-first'
|
||||
import { OAuthCallbackServerOptions } from './types'
|
||||
import express from 'express'
|
||||
import net from 'net'
|
||||
import crypto from 'crypto'
|
||||
|
||||
// Package version from package.json
|
||||
export const MCP_REMOTE_VERSION = require('../../package.json').version
|
||||
|
@ -32,14 +35,21 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo
|
|||
let transportToClientClosed = false
|
||||
let transportToServerClosed = false
|
||||
|
||||
transportToClient.onmessage = (message) => {
|
||||
// @ts-expect-error TODO
|
||||
transportToClient.onmessage = (_message) => {
|
||||
// TODO: fix types
|
||||
const message = _message as any
|
||||
log('[Local→Remote]', message.method || message.id)
|
||||
if (message.method === 'initialize') {
|
||||
const { clientInfo } = message.params
|
||||
if (clientInfo) clientInfo.name = `${clientInfo.name} (via mcp-remote ${MCP_REMOTE_VERSION})`
|
||||
log(JSON.stringify(message, null, 2))
|
||||
}
|
||||
transportToServer.send(message).catch(onServerError)
|
||||
}
|
||||
|
||||
transportToServer.onmessage = (message) => {
|
||||
// @ts-expect-error TODO: fix this type
|
||||
transportToServer.onmessage = (_message) => {
|
||||
// TODO: fix types
|
||||
const message = _message as any
|
||||
log('[Remote→Local]', message.method || message.id)
|
||||
transportToClient.send(message).catch(onClientError)
|
||||
}
|
||||
|
@ -130,14 +140,14 @@ export async function connectToRemoteServer(
|
|||
const sseTransport = transportStrategy === 'sse-only' || transportStrategy === 'sse-first'
|
||||
const transport = sseTransport
|
||||
? new SSEClientTransport(url, {
|
||||
authProvider,
|
||||
requestInit: { headers },
|
||||
eventSourceInit,
|
||||
})
|
||||
authProvider,
|
||||
requestInit: { headers },
|
||||
eventSourceInit,
|
||||
})
|
||||
: new StreamableHTTPClientTransport(url, {
|
||||
authProvider,
|
||||
requestInit: { headers },
|
||||
})
|
||||
authProvider,
|
||||
requestInit: { headers },
|
||||
})
|
||||
|
||||
try {
|
||||
if (client) {
|
||||
|
@ -300,7 +310,16 @@ export function setupOAuthCallbackServerWithLongPoll(options: OAuthCallbackServe
|
|||
log('Auth code received, resolving promise')
|
||||
authCompletedResolve(code)
|
||||
|
||||
res.send('Authorization successful! You may close this window and return to the CLI.')
|
||||
res.send(`
|
||||
Authorization successful!
|
||||
You may close this window and return to the CLI.
|
||||
<script>
|
||||
// If this is a non-interactive session (no manual approval step was required) then
|
||||
// this should automatically close the window. If not, this will have no effect and
|
||||
// the user will see the message above.
|
||||
window.close();
|
||||
</script>
|
||||
`)
|
||||
|
||||
// Notify main flow that auth code is available
|
||||
options.events.emit('auth-code-received', code)
|
||||
|
@ -336,6 +355,27 @@ export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) {
|
|||
return { server, authCode, waitForAuthCode }
|
||||
}
|
||||
|
||||
async function findExistingClientPort(serverUrlHash: string): Promise<number | undefined> {
|
||||
const clientInfo = await readJsonFile<OAuthClientInformationFull>(serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema)
|
||||
if (!clientInfo) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const localhostRedirectUri = clientInfo.redirect_uris.map((uri) => new URL(uri)).find(({ hostname }) => hostname === 'localhost')
|
||||
if (!localhostRedirectUri) {
|
||||
throw new Error('Cannot find localhost callback URI from existing client information')
|
||||
}
|
||||
|
||||
return parseInt(localhostRedirectUri.port)
|
||||
}
|
||||
|
||||
function calculateDefaultPort(serverUrlHash: string): number {
|
||||
// Convert the first 4 bytes of the serverUrlHash into a port offset
|
||||
const offset = parseInt(serverUrlHash.substring(0, 4), 16)
|
||||
// Pick a consistent but random-seeming port from 3335 to 49151
|
||||
return 3335 + (offset % 45816)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an available port on the local machine
|
||||
* @param preferredPort Optional preferred port to try first
|
||||
|
@ -369,14 +409,13 @@ export async function findAvailablePort(preferredPort?: number): Promise<number>
|
|||
/**
|
||||
* Parses command line arguments for MCP clients and proxies
|
||||
* @param args Command line arguments
|
||||
* @param defaultPort Default port for the callback server if specified port is unavailable
|
||||
* @param usage Usage message to show on error
|
||||
* @returns A promise that resolves to an object with parsed serverUrl, callbackPort and headers
|
||||
*/
|
||||
export async function parseCommandLineArgs(args: string[], defaultPort: number, usage: string) {
|
||||
export async function parseCommandLineArgs(args: string[], usage: string) {
|
||||
// Process headers
|
||||
const headers: Record<string, string> = {}
|
||||
let i = 0;
|
||||
let i = 0
|
||||
while (i < args.length) {
|
||||
if (args[i] === '--header' && i < args.length - 1) {
|
||||
const value = args[i + 1]
|
||||
|
@ -423,14 +462,28 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
|
|||
log(usage)
|
||||
process.exit(1)
|
||||
}
|
||||
const serverUrlHash = getServerUrlHash(serverUrl)
|
||||
const defaultPort = calculateDefaultPort(serverUrlHash)
|
||||
|
||||
// Use the specified port, or find an available one
|
||||
const callbackPort = specifiedPort || (await findAvailablePort(defaultPort))
|
||||
// Use the specified port, or the existing client port or fallback to find an available one
|
||||
const [existingClientPort, availablePort] = await Promise.all([findExistingClientPort(serverUrlHash), findAvailablePort(defaultPort)])
|
||||
let callbackPort: number
|
||||
|
||||
if (specifiedPort) {
|
||||
log(`Using specified callback port: ${callbackPort}`)
|
||||
if (existingClientPort && specifiedPort !== existingClientPort) {
|
||||
log(
|
||||
`Warning! Specified callback port of ${specifiedPort}, which conflicts with existing client registration port ${existingClientPort}. Deleting existing client data to force reregistration.`,
|
||||
)
|
||||
await fs.rm(getConfigFilePath(serverUrlHash, 'client_info.json'))
|
||||
}
|
||||
log(`Using specified callback port: ${specifiedPort}`)
|
||||
callbackPort = specifiedPort
|
||||
} else if (existingClientPort) {
|
||||
log(`Using existing client port: ${existingClientPort}`)
|
||||
callbackPort = existingClientPort
|
||||
} else {
|
||||
log(`Using automatically selected callback port: ${callbackPort}`)
|
||||
log(`Using automatically selected callback port: ${availablePort}`)
|
||||
callbackPort = availablePort
|
||||
}
|
||||
|
||||
if (Object.keys(headers).length > 0) {
|
||||
|
@ -468,6 +521,11 @@ export function setupSignalHandlers(cleanup: () => Promise<void>) {
|
|||
|
||||
// Keep the process alive
|
||||
process.stdin.resume()
|
||||
process.stdin.on('end', async () => {
|
||||
log('\nShutting down...')
|
||||
await cleanup()
|
||||
process.exit(0)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -135,7 +135,7 @@ to the CA certificate file. If using claude_desktop_config.json, this might look
|
|||
}
|
||||
|
||||
// Parse command-line arguments and run the proxy
|
||||
parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
|
||||
parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
|
||||
.then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
|
||||
return runProxy(serverUrl, callbackPort, headers, transportStrategy)
|
||||
})
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node", "react"],
|
||||
"types": ["node"],
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue