| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -118,6 +118,11 @@ export interface Host { | |||
| 118 | 118 | * Finds an available TCP port on the system. | |
| 119 | 119 | */ | |
| 120 | 120 | getAvailablePort(): Promise<number>; | |
| 121 | + | ||
| 122 | + /** | ||
| 123 | + * Checks whether a TCP port is available on the system. | ||
| 124 | + */ | ||
| 125 | + isPortAvailable(port: number): Promise<boolean>; | ||
| 121 | 126 | } | |
| 122 | 127 | ||
| 123 | 128 | /** | |
@@ -236,4 +241,16 @@ export const LocalWorkspaceHost: Host = { | |||
| 236 | 241 | }); | |
| 237 | 242 | }); | |
| 238 | 243 | }, | |
| 244 | + | ||
| 245 | + isPortAvailable(port: number): Promise<boolean> { | ||
| 246 | + return new Promise((resolve) => { | ||
| 247 | + const server = createServer(); | ||
| 248 | + server.once('error', () => resolve(false)); | ||
| 249 | + server.listen(port, () => { | ||
| 250 | + server.close(() => { | ||
| 251 | + resolve(true); | ||
| 252 | + }); | ||
| 253 | + }); | ||
| 254 | + }); | ||
| 255 | + }, | ||
| 239 | 256 | }; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -21,4 +21,5 @@ export class MockHost implements Host { | |||
| 21 | 21 | resolveModule = jasmine.createSpy('resolveRequest').and.returnValue('/dev/null'); | |
| 22 | 22 | spawn = jasmine.createSpy('spawn'); | |
| 23 | 23 | getAvailablePort = jasmine.createSpy('getAvailablePort'); | |
| 24 | + isPortAvailable = jasmine.createSpy('isPortAvailable').and.resolveTo(true); | ||
| 24 | 25 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -27,6 +27,9 @@ export function createMockHost(): MockHost { | |||
| 27 | 27 | getAvailablePort: jasmine | |
| 28 | 28 | .createSpy<Host['getAvailablePort']>('getAvailablePort') | |
| 29 | 29 | .and.resolveTo(0), | |
| 30 | + isPortAvailable: jasmine | ||
| 31 | + .createSpy<Host['isPortAvailable']>('isPortAvailable') | ||
| 32 | + .and.resolveTo(true), | ||
| 30 | 33 | } as unknown as MockHost; | |
| 31 | 34 | } | |
| 32 | 35 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,13 @@ import { type McpToolContext, type McpToolDeclaration, declareTool } from '../to | |||
| 15 | 15 | ||
| 16 | 16 | const devserverStartToolInputSchema = z.object({ | |
| 17 | 17 | ...workspaceAndProjectOptions, | |
| 18 | + port: z | ||
| 19 | + .number() | ||
| 20 | + .optional() | ||
| 21 | + .describe( | ||
| 22 | + 'The port number to run the server on. If not provided, a random available port will be chosen. ' + | ||
| 23 | + 'It is recommended to reuse port numbers across calls within the same workspace to maintain consistency.', | ||
| 24 | + ), | ||
| 18 | 25 | }); | |
| 19 | 26 | ||
| 20 | 27 | export type DevserverStartToolInput = z.infer<typeof devserverStartToolInputSchema>; | |
@@ -53,7 +60,17 @@ export async function startDevserver(input: DevserverStartToolInput, context: Mc | |||
| 53 | 60 | }); | |
| 54 | 61 | } | |
| 55 | 62 | ||
| 56 | - const port = await context.host.getAvailablePort(); | ||
| 63 | + let port: number; | ||
| 64 | + if (input.port) { | ||
| 65 | + if (!(await context.host.isPortAvailable(input.port))) { | ||
| 66 | + throw new Error( | ||
| 67 | + `Port ${input.port} is unavailable. Try calling this tool again without the 'port' parameter to auto-assign a free port.`, | ||
| 68 | + ); | ||
| 69 | + } | ||
| 70 | + port = input.port; | ||
| 71 | + } else { | ||
| 72 | + port = await context.host.getAvailablePort(); | ||
| 73 | + } | ||
| 57 | 74 | ||
| 58 | 75 | devserver = new LocalDevserver({ | |
| 59 | 76 | host: context.host, | |
@@ -87,14 +104,18 @@ the first build completes. | |||
| 87 | 104 | background. | |
| 88 | 105 | * **Get Initial Build Logs:** Once a dev server has started, use the "devserver.wait_for_build" tool to ensure it's alive. If there are any | |
| 89 | 106 | build errors, "devserver.wait_for_build" would provide them back and you can give them to the user or rely on them to propose a fix. | |
| 90 | - * **Get Updated Build Logs:** Important: as long as a devserver is alive (i.e. "devserver.stop" wasn't called), after every time you make a | ||
| 91 | - change to the workspace, re-run "devserver.wait_for_build" to see whether the change was successfully built and wait for the devserver to | ||
| 92 | - be updated. | ||
| 107 | + * **Get Updated Build Logs:** Important: as long as a devserver is alive (i.e. "devserver.stop" wasn't called), after every time you | ||
| 108 | + make a change to the workspace, re-run "devserver.wait_for_build" to see whether the change was successfully built and wait for the | ||
| 109 | + devserver to be updated. | ||
| 93 | 110 | </Use Cases> | |
| 94 | 111 | <Operational Notes> | |
| 95 | 112 | * This tool manages development servers by itself. It maintains at most a single dev server instance for each project in the monorepo. | |
| 96 | 113 | * This is an asynchronous operation. Subsequent commands can be ran while the server is active. | |
| 97 | 114 | * Use 'devserver.stop' to gracefully shut down the server and access the full log output. | |
| 115 | + * **Keeping the Server Alive**: It is often better to keep the server alive between tool calls if you expect the user to request more | ||
| 116 | + changes or run more tests, as it saves time on restarts and maintains the file watcher state. You must still call | ||
| 117 | + 'devserver.wait_for_build' after every change to see whether the change was successfully built and be sure that that app was updated. | ||
| 118 | + * **Consistent Ports**: If making multiple calls, it is recommended to reuse the port you got from the first call for subsequent ones. | ||
| 98 | 119 | </Operational Notes> | |
| 99 | 120 | `, | |
| 100 | 121 | isReadOnly: true, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -64,6 +64,31 @@ describe('Serve Tools', () => { | |||
| 64 | 64 | expect(mockProcess.kill).toHaveBeenCalled(); | |
| 65 | 65 | }); | |
| 66 | 66 | ||
| 67 | + it('should use the provided port number', async () => { | ||
| 68 | + const startResult = await startDevserver({ port: 54321 }, mockContext); | ||
| 69 | + expect(startResult.structuredContent.message).toBe( | ||
| 70 | + `Development server for project 'my-app' started and watching for workspace changes.`, | ||
| 71 | + ); | ||
| 72 | + expect(mockHost.spawn).toHaveBeenCalledWith('ng', ['serve', 'my-app', '--port=54321'], { | ||
| 73 | + stdio: 'pipe', | ||
| 74 | + cwd: '/test', | ||
| 75 | + }); | ||
| 76 | + expect(mockHost.getAvailablePort).not.toHaveBeenCalled(); | ||
| 77 | + }); | ||
| 78 | + | ||
| 79 | + it('should throw an error if the provided port is taken', async () => { | ||
| 80 | + mockHost.isPortAvailable.and.resolveTo(false); | ||
| 81 | + | ||
| 82 | + try { | ||
| 83 | + await startDevserver({ port: 55555 }, mockContext); | ||
| 84 | + fail('Should have thrown an error'); | ||
| 85 | + } catch (e) { | ||
| 86 | + expect((e as Error).message).toContain( | ||
| 87 | + "Port 55555 is unavailable. Try calling this tool again without the 'port' parameter to auto-assign a free port.", | ||
| 88 | + ); | ||
| 89 | + } | ||
| 90 | + }); | ||
| 91 | + | ||
| 67 | 92 | it('should wait for a build to complete', async () => { | |
| 68 | 93 | await startDevserver({}, mockContext); | |
| 69 | 94 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments