First I call the client and server using this code:

extends Node

func _ready():
	var args = OS.get_cmdline_args()
	if (args.size() > 1 && args[1] == 'server'):
		$Server.server_setup(13338)
	else:
		$Client.client_setup('127.0.0.1', 13338)

Then server is done like this:

extends Node

func server_setup(port):
	var custom_peer = ENetMultiplayerPeer.new()
	custom_peer.create_server(port, 4000)
	multiplayer.multiplayer_peer = custom_peer

@rpc("any_peer", "call_remote", "reliable")
func client_ready_msg():
	var charid = multiplayer.get_remote_sender_id()
	print("Connected: " + str(charid))
	pass

And client is implemented as follows:

extends Node

func client_setup(ip, port):
	var custom_peer = ENetMultiplayerPeer.new()
	custom_peer.create_client(ip, port)
	multiplayer.multiplayer_peer = custom_peer
	multiplayer.connected_to_server.connect(_on_connection_succeeded)

func _on_connection_succeeded():
	print('Sending ready...')
	await get_tree().create_timer(1).timeout
	get_node('../Server').client_ready_msg.rpc_id(1)

@rpc("any_peer", "call_remote", "reliable")
func client_ready_msg():
	print("This should not be called")
	pass

Notice the get_node('../Server').client_ready_msg.rpc_id(1) ? This is extremely ugly, do I need to put client/server logic in a node that is accessible on both client and server? Notice that I later on declared the function on the client too, but it seems it i not even needed. Has anyone got something similar working without resorting to this kind of hack?

In https://github.com/godotengine/godot/issues/57869#issuecomment-1687411210 someone got it working last year on C#, but the server won't receive the message if I try this.

Okay, one way to solve it is to use another scene for the server, with the same path to some common node both on the client and server. The script can be different (that is what I want) but the path must be the same.